feat: publish reviewed architecture and ADR batch
Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 19s
Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 19s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
This commit is contained in:
parent
023badb512
commit
93608c1f17
120 changed files with 17791 additions and 727 deletions
226
build/adr/activity-core-bounded-operations/v1/index.html
Normal file
226
build/adr/activity-core-bounded-operations/v1/index.html
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="75f42047b51293f7240f8a8c3ec02bf025a47fe6627f106fc4ef22fb1be7459d">
|
||||
<title>Code-registered bounded operations are the only local mutation exception</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-007</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-08-23</span><span>generated from canonical source — do not edit</span></div><h1>Code-registered bounded operations are the only local mutation exception</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-007-bounded-operations.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2027-02-23</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#rejected-alternatives"><span class="n">·</span>Rejected alternatives</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted</strong> (2026-08-23) for ACTIVITY-WP-0035.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The Event Bridge principle says activity-core answers when, what, and where and does not execute domain work. Production nevertheless contains three useful operations whose complete outcome is a small scheduled maintenance action:</p>
|
||||
<ul><li>ingest at most three Repo Manager-selected immutable sources into SBOM Nexus;</li><li>prune Forgejo package versions under live-image protection; and</li><li>invoke the fixed CNPG Option A backup tool for an explicit target list.</li></ul>
|
||||
<p>Historically the latter two ran through a generic <code>shell</code> context resolver. That made a mutating subprocess look like a read and left extension policy to convention. Removing the operations would recreate bespoke cron; accepting arbitrary shell would make activity-core a general executor.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. Bounded operations are a narrow implementation exception</h3>
|
||||
<p>The governing when/what/where responsibility does not gain a general “how.” A bounded operation is allowed only when the operation itself is the declared automation outcome and all admission requirements below are code-reviewable.</p>
|
||||
<p>The initial allowlist is exactly:</p>
|
||||
<ol><li><code>sbom_nexus_ingest</code></li><li><code>forgejo_package_prune</code></li><li><code>cnpg_option_a_backup</code></li></ol>
|
||||
<p>Adding an operation requires updating the code-owned registry, this ADR (or a successor), tests, credential route, and evidence contract. A string in a definition cannot register an operation.</p>
|
||||
<h3>2. Admission is fail closed during file sync</h3>
|
||||
<p>Each registry entry declares:</p>
|
||||
<ul><li>source type and query;</li><li>whether mutation intent is <code>apply</code> or <code>dry_run</code> and that it is explicit;</li><li>fixed or maximum target bounds;</li><li>canonical implementation and allowed configuration;</li><li>idempotency and Temporal retry semantics;</li><li>maximum execution timeout;</li><li>credential owner/route; and</li><li>mandatory non-secret evidence mode.</li></ul>
|
||||
<p>Unknown <code>shell</code> queries are refused unless separately registered as read-only. Known operation queries with missing, malformed, or over-limit safety fields are refused before database projection or Temporal schedule reconciliation.</p>
|
||||
<h3>3. Context resolution is read-only</h3>
|
||||
<p>The workflow first resolves and freezes context. Mutations then run in an explicit bounded-operation stage and merge only normalized outcomes into the snapshot before evidence and rule/instruction evaluation.</p>
|
||||
<p>SBOM selection remains a read in the context phase; its fixed selection is the input to the operation stage. Package prune and backup have no discovery read inside activity-core and bind a pending marker until their operation completes.</p>
|
||||
<h3>4. Retry behavior is operation-specific</h3>
|
||||
<ul><li>SBOM ingest uses stable per-run/per-repository idempotency keys and heartbeat checkpoints, so Activity retries resume the frozen batch.</li><li>Forgejo prune and CNPG backup have no activity-core-verifiable remote idempotency receipt. Their operation activity therefore has one Temporal attempt; a failure remains visible for operator reconciliation rather than risking an automatic second mutation.</li></ul>
|
||||
<p>This does not preclude future safe retries after the platform tools expose a durable operation receipt.</p>
|
||||
<h3>5. Evidence is mandatory and bounded</h3>
|
||||
<p>Every operation must produce an allowlisted summary through a configured report/evidence sink. Raw subprocess output, tokens, provider payloads, archive URLs, kubeconfigs, and credential material are not evidence.</p>
|
||||
</section>
|
||||
<section id="rejected-alternatives"><h2>Rejected alternatives</h2>
|
||||
<ul><li><strong>Keep mutating shell resolvers.</strong> Rejected because resolution should be a replayable read and the generic dispatcher hides mutation admission.</li><li><strong>Generic command activity.</strong> Rejected because command text/path from a definition is remote code execution by configuration.</li><li><strong>Move every operation to a rein.</strong> Rejected for these fixed platform operations; it adds an agent execution constellation without judgement or repository work. Operations that exceed this ADR's bounds do belong there.</li><li><strong>Remove all local operations.</strong> Rejected because it recreates scattered cron and loses Temporal/evidence guarantees for established maintenance.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Definition parsing gains a central operation-policy validator.</li><li>The workflow gains an explicit operation stage.</li><li>The generic <code>shell</code> resolver becomes read-only.</li><li>Existing definitions migrate without widening targets or permissions.</li><li>The registry is intentionally small and architectural review is required to expand it.</li></ul>
|
||||
</section><footer><span>ACT-ADR-007 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-007-bounded-operations.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ea6fb0e92a25825b42e00b2f85aa23cf50605294">
|
||||
<meta name="policy-source-digest" content="75f42047b51293f7240f8a8c3ec02bf025a47fe6627f106fc4ef22fb1be7459d">
|
||||
<title>Code-registered bounded operations are the only local mutation exception</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-007</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-08-23</span><span>generated from canonical source — do not edit</span></div><h1>Code-registered bounded operations are the only local mutation exception</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-007-bounded-operations.md · ea6fb0e92a25825b42e00b2f85aa23cf50605294</code></p><p class="sub">Review due: 2027-02-23</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#rejected-alternatives"><span class="n">·</span>Rejected alternatives</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted</strong> (2026-08-23) for ACTIVITY-WP-0035.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The Event Bridge principle says activity-core answers when, what, and where and does not execute domain work. Production nevertheless contains three useful operations whose complete outcome is a small scheduled maintenance action:</p>
|
||||
<ul><li>ingest at most three Repo Manager-selected immutable sources into SBOM Nexus;</li><li>prune Forgejo package versions under live-image protection; and</li><li>invoke the fixed CNPG Option A backup tool for an explicit target list.</li></ul>
|
||||
<p>Historically the latter two ran through a generic <code>shell</code> context resolver. That made a mutating subprocess look like a read and left extension policy to convention. Removing the operations would recreate bespoke cron; accepting arbitrary shell would make activity-core a general executor.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. Bounded operations are a narrow implementation exception</h3>
|
||||
<p>The governing when/what/where responsibility does not gain a general “how.” A bounded operation is allowed only when the operation itself is the declared automation outcome and all admission requirements below are code-reviewable.</p>
|
||||
<p>The initial allowlist is exactly:</p>
|
||||
<ol><li><code>sbom_nexus_ingest</code></li><li><code>forgejo_package_prune</code></li><li><code>cnpg_option_a_backup</code></li></ol>
|
||||
<p>Adding an operation requires updating the code-owned registry, this ADR (or a successor), tests, credential route, and evidence contract. A string in a definition cannot register an operation.</p>
|
||||
<h3>2. Admission is fail closed during file sync</h3>
|
||||
<p>Each registry entry declares:</p>
|
||||
<ul><li>source type and query;</li><li>whether mutation intent is <code>apply</code> or <code>dry_run</code> and that it is explicit;</li><li>fixed or maximum target bounds;</li><li>canonical implementation and allowed configuration;</li><li>idempotency and Temporal retry semantics;</li><li>maximum execution timeout;</li><li>credential owner/route; and</li><li>mandatory non-secret evidence mode.</li></ul>
|
||||
<p>Unknown <code>shell</code> queries are refused unless separately registered as read-only. Known operation queries with missing, malformed, or over-limit safety fields are refused before database projection or Temporal schedule reconciliation.</p>
|
||||
<h3>3. Context resolution is read-only</h3>
|
||||
<p>The workflow first resolves and freezes context. Mutations then run in an explicit bounded-operation stage and merge only normalized outcomes into the snapshot before evidence and rule/instruction evaluation.</p>
|
||||
<p>SBOM selection remains a read in the context phase; its fixed selection is the input to the operation stage. Package prune and backup have no discovery read inside activity-core and bind a pending marker until their operation completes.</p>
|
||||
<h3>4. Retry behavior is operation-specific</h3>
|
||||
<ul><li>SBOM ingest uses stable per-run/per-repository idempotency keys and heartbeat checkpoints, so Activity retries resume the frozen batch.</li><li>Forgejo prune and CNPG backup have no activity-core-verifiable remote idempotency receipt. Their operation activity therefore has one Temporal attempt; a failure remains visible for operator reconciliation rather than risking an automatic second mutation.</li></ul>
|
||||
<p>This does not preclude future safe retries after the platform tools expose a durable operation receipt.</p>
|
||||
<h3>5. Evidence is mandatory and bounded</h3>
|
||||
<p>Every operation must produce an allowlisted summary through a configured report/evidence sink. Raw subprocess output, tokens, provider payloads, archive URLs, kubeconfigs, and credential material are not evidence.</p>
|
||||
</section>
|
||||
<section id="rejected-alternatives"><h2>Rejected alternatives</h2>
|
||||
<ul><li><strong>Keep mutating shell resolvers.</strong> Rejected because resolution should be a replayable read and the generic dispatcher hides mutation admission.</li><li><strong>Generic command activity.</strong> Rejected because command text/path from a definition is remote code execution by configuration.</li><li><strong>Move every operation to a rein.</strong> Rejected for these fixed platform operations; it adds an agent execution constellation without judgement or repository work. Operations that exceed this ADR's bounds do belong there.</li><li><strong>Remove all local operations.</strong> Rejected because it recreates scattered cron and loses Temporal/evidence guarantees for established maintenance.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Definition parsing gains a central operation-policy validator.</li><li>The workflow gains an explicit operation stage.</li><li>The generic <code>shell</code> resolver becomes read-only.</li><li>Existing definitions migrate without widening targets or permissions.</li><li>The registry is intentionally small and architectural review is required to expand it.</li></ul>
|
||||
</section><footer><span>ACT-ADR-007 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-007-bounded-operations.md · ea6fb0e92a25825b42e00b2f85aa23cf50605294</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
|
||||
<meta name="policy-source-digest" content="157a53907240733148137338c9826a56b77d04d4f41e59c6cb57e6b6f8d9534d">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="1670e1616094bc6f2c0c7d47b2e019c02d5870b22b564ef83afa24caff197bab">
|
||||
<title>Markdown-as-Definition Format for Event Types and ActivityDefinitions</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-002</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Markdown-as-Definition Format for Event Types and ActivityDefinitions</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-002-definition-format.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-002</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Markdown-as-Definition Format for Event Types and ActivityDefinitions</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-002-definition-format.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -354,7 +354,7 @@ trusted_fields:
|
|||
- event.attributes.domain
|
||||
- event.attributes.tags
|
||||
model: claude-sonnet-4-6
|
||||
review_required: false
|
||||
review_advisory: false
|
||||
prompt: |
|
||||
A new repository has been registered in the Coulomb organization.
|
||||
|
||||
|
|
@ -434,4 +434,4 @@ None (unassigned)</pre>
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ACT-ADR-001 — Event Bridge Architecture</li><li>ACT-ADR-003 — Rule vs. Instruction model and DSL</li><li>CUST-ADR-001 — Workplans as repository artefacts</li></ul>
|
||||
</section><footer><span>ACT-ADR-002 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-002-definition-format.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>
|
||||
</section><footer><span>ACT-ADR-002 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-002-definition-format.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,437 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="1670e1616094bc6f2c0c7d47b2e019c02d5870b22b564ef83afa24caff197bab">
|
||||
<title>Markdown-as-Definition Format for Event Types and ActivityDefinitions</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-002</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Markdown-as-Definition Format for Event Types and ActivityDefinitions</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-002-definition-format.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Event type schemas and ActivityDefinition rules need to be understood and authored by three distinct audiences simultaneously: humans reviewing and debugging automation, agents creating and modifying definitions at runtime, and machines parsing and evaluating them. Traditional approaches split these concerns — schemas go in JSON Schema or YAML, documentation goes in a wiki, logic goes in code — and they drift apart. A bug in a rule requires cross-referencing three places to understand intent, check the schema, and read the condition.</p>
|
||||
<p>The Custodian ecosystem already uses markdown files with YAML frontmatter as the authoritative format for workplans, ADRs, SCOPE.md, and INTENT.md — all understood by humans and agents without additional tooling. The same pattern should apply here.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>Event type definitions and ActivityDefinitions are markdown files</strong> where machine- parseable structure (frontmatter YAML and fenced definition blocks) is embedded within human-readable narrative. Intent, schema, logic, and debugging notes live in one file.</p>
|
||||
<h3>Event Type Definition Files</h3>
|
||||
<p><strong>Location</strong>: <code>event-types/{namespace}.{event-name}.md</code> within the activity-core repo (or a registered event-types registry repo if volumes justify separation).</p>
|
||||
<p><strong>Naming convention</strong>: <code>{publisher-domain}.{noun}.{verb}.md</code>, e.g.:</p>
|
||||
<ul><li><code>org.repo.registered.md</code></li><li><code>org.security.cve.published.md</code></li><li><code>org.workstream.completed.md</code></li></ul>
|
||||
<p><strong>Structure</strong>:</p>
|
||||
<pre>---
|
||||
id: org.repo.registered
|
||||
type: event-type
|
||||
version: "1.0"
|
||||
publisher: the-custodian/state-hub
|
||||
governance: publisher-declared # publisher-declared | curated
|
||||
status: active # active | deprecated | draft
|
||||
introduced: "2026-05-14"
|
||||
---
|
||||
|
||||
# Event: org.repo.registered
|
||||
|
||||
## Intent
|
||||
|
||||
One-paragraph statement of why this event exists and what it signals.
|
||||
Written for an agent or human who has never seen it before.
|
||||
|
||||
## When Published
|
||||
|
||||
Bulleted list of the exact conditions under which the publisher fires this event.
|
||||
Be precise — ambiguity here causes missed or duplicate activations.
|
||||
|
||||
## Attributes
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `repo_slug` | string | yes | URL-safe repository identifier |
|
||||
| `domain` | string | yes | Domain slug the repo is assigned to |
|
||||
| `tags` | string[] | no | Capability tags set at registration time |
|
||||
| `registered_at` | datetime | yes | ISO 8601 UTC timestamp |
|
||||
|
||||
## Example Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "evt-7f3a1b2c",
|
||||
"type": "org.repo.registered",
|
||||
"version": "1.0",
|
||||
"timestamp": "2026-05-14T10:00:00Z",
|
||||
"publisher": "the-custodian/state-hub",
|
||||
"attributes": {
|
||||
"repo_slug": "new-python-service",
|
||||
"domain": "railiance",
|
||||
"tags": ["python-service", "fastapi"],
|
||||
"registered_at": "2026-05-14T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Consumer Notes
|
||||
|
||||
Guidance for agents and humans writing rules against this event type:
|
||||
- Which attributes are safe for instruction prompts (trusted fields)
|
||||
- Common misuses or gotchas
|
||||
- Related events that are often used together
|
||||
|
||||
## Debugging
|
||||
|
||||
What to check when an activity that subscribes to this event does not fire:
|
||||
- How to verify the event was published (NATS subject, log entry)
|
||||
- How to inspect the event payload in the registry
|
||||
- Common schema validation failures</pre>
|
||||
<h3>Attribute Types</h3>
|
||||
<p>The type system for event attributes is intentionally small:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Type</th><th>Notes</th></tr></thead><tbody><tr><td><code>string</code></td><td>UTF-8 string</td></tr><tr><td><code>integer</code></td><td>64-bit signed integer</td></tr><tr><td><code>float</code></td><td>64-bit float</td></tr><tr><td><code>boolean</code></td><td>true / false</td></tr><tr><td><code>datetime</code></td><td>ISO 8601 UTC string in payload, parsed to datetime in evaluator</td></tr><tr><td><code>uuid</code></td><td>String in payload, validated as UUID v4</td></tr><tr><td><code>string[]</code></td><td>JSON array of strings</td></tr><tr><td><code>integer[]</code></td><td>JSON array of integers</td></tr><tr><td><code>object</code></td><td>Freeform JSON object — cannot be used in rule conditions; instruction-only</td></tr></tbody></table></div>
|
||||
<p><code>object</code> type attributes are available to instructions but excluded from rule conditions deliberately — rules must be deterministic and schema-validatable.</p>
|
||||
<h3>ActivityDefinition Files</h3>
|
||||
<p><strong>Location</strong>: <code>activity-definitions/{slug}.md</code> within the repo that owns the automation. For org-wide automations: <code>activity-core/activity-definitions/</code>. For domain-specific automations: <code>{domain-repo}/activity-definitions/</code>.</p>
|
||||
<p><strong>Structure</strong>:</p>
|
||||
<pre>---
|
||||
id: ACT-DEF-onboard-python-repo
|
||||
type: activity-definition
|
||||
version: "1.0"
|
||||
status: active
|
||||
trigger:
|
||||
type: event # event | cron | scheduled
|
||||
event_type: org.repo.registered # for type: event
|
||||
# cron: "0 9 * * 1" # for type: cron (5-field, UTC)
|
||||
# timezone: "Europe/Berlin" # optional, cron only
|
||||
# misfire_policy: skip # skip | catchup | compress (cron only)
|
||||
# at: "2026-06-01T09:00:00Z" # for type: scheduled (one-off)
|
||||
context_sources:
|
||||
- type: repo-scoping
|
||||
query: repo_profile
|
||||
bind_to: context.repo_profile
|
||||
- type: state-hub
|
||||
query: domain_summary
|
||||
bind_to: context.domain_summary
|
||||
governance: publisher-declared
|
||||
owner: custodian-agent
|
||||
created: "2026-05-14"
|
||||
---
|
||||
|
||||
# ActivityDefinition: Onboard New Python Service
|
||||
|
||||
## Purpose
|
||||
|
||||
One paragraph. What does this automation do and why does it exist? What problem
|
||||
would accumulate if this automation were turned off?
|
||||
|
||||
## Trigger
|
||||
|
||||
Which event type fires this activity, and under what conditions does it apply?
|
||||
Cross-reference the event type definition file.
|
||||
|
||||
## Context Sources
|
||||
|
||||
What context is resolved before rules are evaluated? Explain what each source
|
||||
provides and why it is needed.
|
||||
|
||||
## Rules
|
||||
|
||||
Each rule is a fenced block tagged `rule`. Rules are evaluated in order; all
|
||||
matching rules fire (not first-match-only). See ACT-ADR-003 for the expression
|
||||
language specification.
|
||||
|
||||
```rule
|
||||
id: create-sbom-scan
|
||||
condition: '"python-service" in event.attributes.tags'
|
||||
action:
|
||||
task_template: tasks/sbom-initial-scan.md
|
||||
target_repo: event.attributes.repo_slug
|
||||
priority: high
|
||||
labels: ["onboarding", "security"]
|
||||
```
|
||||
|
||||
```rule
|
||||
id: create-scope-generation
|
||||
condition: '"python-service" in event.attributes.tags and context.repo_profile.scope_md_exists == false'
|
||||
action:
|
||||
task_template: tasks/generate-scope-md.md
|
||||
target_repo: event.attributes.repo_slug
|
||||
priority: medium
|
||||
labels: ["onboarding", "documentation"]
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
Instructions are evaluated after all rules. An instruction asks an LLM to decide
|
||||
what additional tasks (if any) to create. See ACT-ADR-003 for safety requirements.
|
||||
|
||||
```instruction
|
||||
id: domain-specific-onboarding
|
||||
condition: 'event.attributes.domain != "test_domain_v2"'
|
||||
trusted_fields:
|
||||
- event.attributes.repo_slug
|
||||
- event.attributes.domain
|
||||
- event.attributes.tags
|
||||
model: claude-sonnet-4-6
|
||||
review_advisory: false
|
||||
prompt: |
|
||||
A new repository has been registered in the Coulomb organization.
|
||||
|
||||
Repository: {event.attributes.repo_slug}
|
||||
Domain: {event.attributes.domain}
|
||||
Tags: {event.attributes.tags}
|
||||
|
||||
Based on the domain's current standards and the repository profile above,
|
||||
determine what additional domain-specific onboarding tasks should be created
|
||||
beyond the standard SBOM scan and SCOPE.md generation. Return an empty list
|
||||
if no additional tasks are warranted.
|
||||
output_schema: tasks/task-template-list-schema.json
|
||||
```
|
||||
|
||||
## Task Templates
|
||||
|
||||
References to task template files used in rule actions. Each template is a
|
||||
separate markdown file under `tasks/` that defines the task title, description
|
||||
template, default labels, and default assignee logic.
|
||||
|
||||
- `tasks/sbom-initial-scan.md`
|
||||
- `tasks/generate-scope-md.md`
|
||||
|
||||
## Notes
|
||||
|
||||
Operational notes, edge cases, and context that does not fit elsewhere.
|
||||
|
||||
## Debugging
|
||||
|
||||
Checklist for when this ActivityDefinition fires but produces unexpected output:
|
||||
|
||||
1. Was the triggering event published with the correct type and attributes?
|
||||
2. Do the rule conditions evaluate as expected? (Use `make eval-rule` with a fixture)
|
||||
3. Is issue-core reachable and configured for the target domain?
|
||||
4. For instructions: check the audit log for the model response and output validation result.
|
||||
|
||||
## Change History
|
||||
|
||||
- v1.0 (2026-05-14): Initial definition</pre>
|
||||
<h3>Governance model</h3>
|
||||
<p>The <code>governance</code> field on an event type definition determines how the registry runtime handles it:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Value</th><th>Behaviour</th></tr></thead><tbody><tr><td><code>publisher-declared</code></td><td>Accepted immediately on publish; no review required</td></tr><tr><td><code>curated</code></td><td>Held in <code>pending</code> state until a curator approves via registry API</td></tr></tbody></table></div>
|
||||
<p>The runtime checks the <strong>environment's curator gate configuration</strong> — not just the file's governance field. An environment configured with <code>curator_gate: disabled</code> treats all event types as <code>publisher-declared</code> regardless of the field value. An environment with <code>curator_gate: required</code> treats all event types as <code>curated</code> regardless of the field value. The field is the publisher's declared preference; the environment config is the enforcement point.</p>
|
||||
<p>This means:</p>
|
||||
<ul><li><strong>Dev / integration</strong>: <code>curator_gate: disabled</code> — developers and agents iterate freely; new event types take effect immediately.</li><li><strong>Staging / production</strong>: <code>curator_gate: required</code> — all new event types queue for curator review before the runtime accepts events of that type.</li></ul>
|
||||
<h3>File as source of truth</h3>
|
||||
<p>Following CUST-ADR-001 (Workplans as Repository Artefacts), definition files are the canonical source of truth. The activity-core runtime indexes them into its database on startup and via a sync command. The database is a queryable cache, not the origin. A definition deleted from the filesystem is disabled at next sync.</p>
|
||||
<h3>Task Templates</h3>
|
||||
<p>Task templates are separate markdown files (<code>tasks/{slug}.md</code>) referenced from ActivityDefinition action blocks. They define:</p>
|
||||
<pre>---
|
||||
id: tasks/sbom-initial-scan
|
||||
type: task-template
|
||||
---
|
||||
# Task: Run Initial SBOM Scan
|
||||
|
||||
## Title template
|
||||
`Run SBOM scan — {target_repo}`
|
||||
|
||||
## Description template
|
||||
Initial SBOM scan required for newly registered repository `{target_repo}`.
|
||||
Run: `make ingest-sbom REPO={target_repo} SCAN=1`
|
||||
|
||||
## Default labels
|
||||
["sbom", "security", "automated"]
|
||||
|
||||
## Default assignee
|
||||
None (unassigned)</pre>
|
||||
<p>This keeps task content editable separately from the routing logic in ActivityDefinitions.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>A new <code>event-types/</code> directory in activity-core (and eventually a shared registry) holds all org event type definitions.</li><li>A new <code>activity-definitions/</code> directory in activity-core holds org-wide automations.</li><li>Domain repos may hold their own <code>activity-definitions/</code> for domain-specific automations, scanned by activity-core at sync time.</li><li>The runtime requires a parser for the <code>rule</code> and <code>instruction</code> fenced blocks.</li><li>SCOPE.md for activity-core must be updated to list these directories.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<p><strong>Pure JSON Schema for event types, separate wiki for docs</strong>: rejected — documentation and schema diverge immediately; agents must cross-reference two systems to author a rule correctly.</p>
|
||||
<p><strong>OpenAPI / AsyncAPI specification</strong>: rejected — those formats are excellent for API and broker documentation but not designed for co-locating operational intent and debugging guidance. They are also less readable for non-specialists.</p>
|
||||
<p><strong>Code-only (Python dataclasses for event schemas, Python functions for rules)</strong>: rejected — requires code deployment for any definition change; agents cannot modify definitions without write access to the codebase; non-technical stakeholders cannot review or understand automation policies.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ACT-ADR-001 — Event Bridge Architecture</li><li>ACT-ADR-003 — Rule vs. Instruction model and DSL</li><li>CUST-ADR-001 — Workplans as repository artefacts</li></ul>
|
||||
</section><footer><span>ACT-ADR-002 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-002-definition-format.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
|
||||
<meta name="policy-source-digest" content="ac70015255b8972c7ee38f1a0fb934c6aa5f397634ddc298f0878a8eed6a774a">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="57ecf490465e71cc4970133301cc315748b1d6ee65caac3690472ff88f3aab06">
|
||||
<title>Activity-Core as Coulomb Org Event Bridge</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-001</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Activity-Core as Coulomb Org Event Bridge</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-001-event-bridge-architecture.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#trigger-types"><span class="n">·</span>Trigger types</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-001</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Activity-Core as Coulomb Org Event Bridge</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-001-event-bridge-architecture.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#trigger-types"><span class="n">·</span>Trigger types</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -230,7 +230,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<h3>Immediate</h3>
|
||||
<ul><li>activity-core's <code>INTENT.md</code> and <code>SCOPE.md</code> are rewritten to reflect this architecture.</li><li>The <code>task_instances</code> Postgres table is reclassified as a <strong>spawn audit trail</strong> — it records the act of spawning (what was created, when, which issue-core reference) but is not the authoritative task record. Authoritative lifecycle state lives in issue-core.</li><li>A task emission adapter interface (<code>src/activity_core/issue_sink.py</code>) replaces any direct Postgres writes to <code>task_instances</code> with calls through the adapter.</li><li>The <code>TaskExecutorWorkflow</code> stub from WP-0001 is replaced with the actual adapter call in WP-0003.</li></ul>
|
||||
<ul><li>activity-core's <code>INTENT.md</code> and <code>SCOPE.md</code> are rewritten to reflect this architecture.</li><li><code>task_spawn_log</code> is the local <strong>spawn audit trail</strong>; authoritative work-item lifecycle state lives downstream.</li><li>A task emission adapter (<code>src/activity_core/issue_sink.py</code>) owns downstream creation. The unused <code>TaskExecutorWorkflow</code> and <code>task_instances</code> compatibility surface was retired by ACTIVITY-WP-0035.</li></ul>
|
||||
<h3>Medium term</h3>
|
||||
<ul><li>State hub adds NATS publishing to its lifecycle operations.</li><li>Gitea webhook receiver added to activity-core as a new HTTP router.</li><li>Existing state hub maintenance crons are migrated to ActivityDefinitions.</li><li>issue-facade is renamed issue-core and re-registered under the <code>capabilities</code> domain.</li></ul>
|
||||
<h3>Long term</h3>
|
||||
|
|
@ -244,4 +244,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ACT-ADR-002 — Event type and ActivityDefinition definition format</li><li>ACT-ADR-003 — Rule vs. Instruction model and DSL</li><li>CUST-ADR-001 — Workplans as repository artefacts (canon/architecture/)</li><li>CUST-TFE-SCOPE-2026-000001 — task-flow-engine extraction plan (canon/projects/)</li><li>activity-core INTENT.md (to be written)</li><li>activity-core WP-0003 (to be written)</li></ul>
|
||||
</section><footer><span>ACT-ADR-001 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-001-event-bridge-architecture.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>
|
||||
</section><footer><span>ACT-ADR-001 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-001-event-bridge-architecture.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="57ecf490465e71cc4970133301cc315748b1d6ee65caac3690472ff88f3aab06">
|
||||
<title>Activity-Core as Coulomb Org Event Bridge</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-001</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Activity-Core as Coulomb Org Event Bridge</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-001-event-bridge-architecture.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#trigger-types"><span class="n">·</span>Trigger types</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The Coulomb organization's set of repositories, services, and deployments is growing beyond what a single person can coordinate manually. The state hub tracks cross-domain state but has no mechanism to automatically respond to it. Recurring maintenance (dependency scans, SBOM staleness checks, consistency audits) is implemented as bespoke cron jobs baked into individual services — scattered, hard to audit, and impossible to govern from a single vantage point.</p>
|
||||
<p>Three forces drive the need for a dedicated orchestration layer:</p>
|
||||
<ol><li><strong>Scale</strong>: as the repo count grows, manual coordination becomes the bottleneck.</li><li><strong>Reactivity</strong>: org-level events (new repo registered, CVE published, deployment completed) should trigger coordinated responses without human intervention.</li><li><strong>Separation of concerns</strong>: the state hub is a read model and should remain one. It must not accumulate automation logic to avoid becoming a God object.</li></ol>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>activity-core is the org-wide Event Bridge for the Coulomb organization.</strong></p>
|
||||
<p>Its responsibility is exactly three things:</p>
|
||||
<ol><li><strong>Receive events</strong> — time-based (cron, one-off scheduled) and domain events (NATS, Gitea webhooks, state hub lifecycle signals).</li><li><strong>Evaluate rules and instructions</strong> — given event payload and resolved context, determine what work must be created.</li><li><strong>Emit task sets</strong> — publish structured task creation requests to issue-core.</li></ol>
|
||||
<p>It does not execute work. It does not track task lifecycle. It does not manage projects.</p>
|
||||
<h3>Boundary rules</h3>
|
||||
<div class="scroll"><table><thead><tr><th>Concern</th><th>Owner</th></tr></thead><tbody><tr><td>Cross-org task scheduling and reactive automation</td><td><strong>activity-core</strong></td></tr><tr><td>Task lifecycle (create, assign, track, close)</td><td><strong>issue-core</strong></td></tr><tr><td>Project and initiative management (phased, completion-gated)</td><td><strong>project-core</strong> (future)</td></tr><tr><td>Repository capability profiling</td><td><strong>repo-scoping</strong></td></tr><tr><td>Cross-domain coordination state</td><td><strong>state hub</strong></td></tr><tr><td>Execution of automatable tasks</td><td>Temporal workers (per-repo)</td></tr></tbody></table></div>
|
||||
<h3>Event type registry</h3>
|
||||
<p>Event types are declared by publishers as markdown definition files (see ACT-ADR-002). Governance is <strong>publisher-declared by default</strong>: a publisher registers its event types by committing definition files to the event-types registry. In production environments, a curator gate can be enabled — registry entries must be reviewed before the runtime accepts events of that type. This is a configuration flag per runtime scope (dev, staging, prod), not a hard-coded rule.</p>
|
||||
<h3>State hub relationship</h3>
|
||||
<p>The state hub <strong>delegates automation to activity-core</strong> rather than implementing it internally. Concretely:</p>
|
||||
<ul><li>Maintenance jobs currently baked into the state hub (consistency sync, SBOM staleness checks) are migrated to ActivityDefinitions in activity-core.</li><li>The state hub becomes a <strong>publisher</strong> of lifecycle events on NATS (<code>org.workstream.created</code>, <code>org.decision.resolved</code>, <code>org.repo.registered</code>, etc.).</li><li>The state hub does not subscribe to activity-core's output directly; it reads task state from issue-core when needed.</li></ul>
|
||||
<p>This preserves the state hub as a read model and makes activity-core the single home for automation policy.</p>
|
||||
<h3>rules-core: module-first</h3>
|
||||
<p>The rule and instruction evaluation engine starts as <code>src/activity_core/rules/</code> — a module with a clean internal boundary (no imports from Temporal, Postgres, or FastAPI within the module). Extraction to a standalone <code>rules-core</code> repository happens when a <strong>second consumer</strong> (e.g. state hub governance, project-core) needs the engine. This follows the same discipline as the task-flow-engine extraction plan (CUST-TFE-SCOPE).</p>
|
||||
<h3>NATS as org infrastructure</h3>
|
||||
<p>NATS JetStream is promoted from an activity-core internal component to <strong>org-wide event bus infrastructure</strong>. It runs as a standalone service (not bundled in activity-core's docker-compose) with its own lifecycle. All services that publish or subscribe to org events do so via NATS streams.</p>
|
||||
<h3>issue-core integration</h3>
|
||||
<p>activity-core communicates with issue-core via a <strong>task emission adapter</strong> — an abstraction layer that, in the initial implementation, calls issue-core's REST API. The adapter interface is defined now; the transport can migrate to NATS subscription (issue-core subscribes to <code>task.spawned</code> events) once issue-core adds that capability. This avoids hardcoding REST coupling throughout the codebase.</p>
|
||||
<h3>Webhook receiver</h3>
|
||||
<p>A new HTTP endpoint within activity-core accepts inbound webhooks from Gitea (and later GitHub, other services). It normalises payloads to the canonical EventEnvelope format, validates against the event type registry, and publishes to NATS. This runs alongside the existing FastAPI <code>api.py</code>.</p>
|
||||
<h3>Domain assignment</h3>
|
||||
<p>activity-core and issue-core are assigned to the <strong><code>capabilities</code></strong> domain — the same domain as repo-scoping. These are org-wide infrastructure tools that serve all domains equally, not artefacts of any single project or custodian's personal workflow. issue-core is explicitly disassociated from the markitect domain.</p>
|
||||
</section>
|
||||
<section id="trigger-types"><h2>Trigger types</h2>
|
||||
<p>Three trigger types are supported:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Type</th><th>Description</th><th>Temporal mechanism</th></tr></thead><tbody><tr><td><code>cron</code></td><td>Recurring schedule (5-field cron + timezone + misfire policy)</td><td>Temporal Schedule (implemented WP-0002)</td></tr><tr><td><code>event</code></td><td>React to a named event type on NATS</td><td>Temporal workflow started by Event Router</td></tr><tr><td><code>scheduled</code></td><td>One-off at a future datetime</td><td>Temporal Schedule with <code>remaining_actions: 1</code></td></tr></tbody></table></div>
|
||||
<p><code>scheduled</code> is a new trigger type added in WP-0003.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<h3>Immediate</h3>
|
||||
<ul><li>activity-core's <code>INTENT.md</code> and <code>SCOPE.md</code> are rewritten to reflect this architecture.</li><li><code>task_spawn_log</code> is the local <strong>spawn audit trail</strong>; authoritative work-item lifecycle state lives downstream.</li><li>A task emission adapter (<code>src/activity_core/issue_sink.py</code>) owns downstream creation. The unused <code>TaskExecutorWorkflow</code> and <code>task_instances</code> compatibility surface was retired by ACTIVITY-WP-0035.</li></ul>
|
||||
<h3>Medium term</h3>
|
||||
<ul><li>State hub adds NATS publishing to its lifecycle operations.</li><li>Gitea webhook receiver added to activity-core as a new HTTP router.</li><li>Existing state hub maintenance crons are migrated to ActivityDefinitions.</li><li>issue-facade is renamed issue-core and re-registered under the <code>capabilities</code> domain.</li></ul>
|
||||
<h3>Long term</h3>
|
||||
<ul><li>rules-core extracted as a standalone package when a second consumer appears.</li><li>project-core created (depends on task-flow-engine extraction) for multi-phase initiative management — explicitly out of scope for activity-core.</li><li>NATS gets its own operational runbook and monitoring as org infrastructure.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<p><strong>State hub absorbs activity-core functionality</strong>: rejected — turns the state hub into a God object, violates the read-model boundary, and makes automation logic impossible to test independently.</p>
|
||||
<p><strong>Per-repo automation (GitHub Actions style)</strong>: rejected — cross-repo coordination requires a single vantage point that can see all repos; per-repo actions can't express org-level triggers or context.</p>
|
||||
<p><strong>Activity-core as a thin Temporal wrapper only</strong>: rejected — without the event type registry and rule model, it's just a scheduler. The governance and introspection properties are the point.</p>
|
||||
<p><strong>Separate rules-core from day one</strong>: rejected — premature extraction adds dependency management overhead before a second consumer exists. Module-first with a clean boundary costs nothing and preserves the extraction option.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ACT-ADR-002 — Event type and ActivityDefinition definition format</li><li>ACT-ADR-003 — Rule vs. Instruction model and DSL</li><li>CUST-ADR-001 — Workplans as repository artefacts (canon/architecture/)</li><li>CUST-TFE-SCOPE-2026-000001 — task-flow-engine extraction plan (canon/projects/)</li><li>activity-core INTENT.md (to be written)</li><li>activity-core WP-0003 (to be written)</li></ul>
|
||||
</section><footer><span>ACT-ADR-001 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-001-event-bridge-architecture.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
228
build/adr/activity-core-glas-profile-execution/v1/index.html
Normal file
228
build/adr/activity-core-glas-profile-execution/v1/index.html
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="88041e9eb3a0f91fdd9da46f9813d7304b5ba58e62f9d28ea8f63c0a7b055b63">
|
||||
<title>Profile-driven execution selection over the ops_run pull queue</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-006</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-08-21</span><span>generated from canonical source — do not edit</span></div><h1>Profile-driven execution selection over the ops_run pull queue</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-006-glas-profile-execution.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2027-02-21</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted</strong> (2026-08-21). Hub decision <code>147beec6-7fe7-4837-8e3a-4264a240379d</code> ("Glas contract 1.0 makes execution constellation selection explicit").</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ACT-ADR-005 gave internal scheduled automation a claimable <code>ops_run</code> plane. What an ops_run said about <em>how</em> to execute was <code>approach_hint</code> — free text, interpreted by the claiming executor at claim time.</p>
|
||||
<p>That binding is too late. Live failure, railiance01 2026-08-17:</p>
|
||||
<pre>state=failed attempt=1 claim_owner=rein-aharness@railiance01
|
||||
title=Run SBOM rescan for binect-js
|
||||
failure=no approach matched labels/definition;
|
||||
labels=['sbom','security','automated']</pre>
|
||||
<p>The run was claimed and leased, <em>then</em> died because nothing could execute it. A claim was consumed to discover the request was unroutable.</p>
|
||||
<p><code>glas-harness</code> GLAS-WP-0004 closed contract 1.0 and proved it by running one bounded task through two different reins (<code>rein-aharness</code>, <code>rein-openweights</code>) via an explicit <code>harness_profile_ref</code>, with real commits and verified sandbox destruction. Meanwhile every activity-core claim in the week to 2026-08-21 was <code>rein-aharness@railiance01</code> — a coupling that is now avoidable rather than inherent.</p>
|
||||
<p>Two shapes were considered: keep the pull queue and carry the profile as payload, or have activity-core call the Glas gateway directly. glas-harness, which owns the contract, advised the first for the initial migration.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. The queue stays; the payload changes</h3>
|
||||
<p>activity-core continues to insert <code>ops_run(open)</code> and executors continue to claim under lease. Scheduling topology does not change. What changes is that a queued run carries an exact <strong>versioned <code>harness_profile_ref</code></strong> plus the correlation / assignment / role / duty / goal / resource-envelope references. The claiming executor passes that request into Glas, which resolves or refuses it <strong>before sandbox creation</strong>.</p>
|
||||
<p>This changes the execution contract without simultaneously changing the scheduling topology — one variable at a time. A direct activity-core → Glas call may be evaluated later, independently, if pull delivery stops meeting operational needs.</p>
|
||||
<h3>2. <code>harness_profile_ref</code> is authoritative; <code>approach_hint</code> is legacy</h3>
|
||||
<p>The two coexist with <strong>distinct semantics</strong>, not as fallbacks for one another:</p>
|
||||
<ul><li><code>harness_profile_ref</code> is the authoritative execution-constellation selector.</li><li><code>approach_hint</code> remains only a legacy activity/definition-matching hint while producers and consumers migrate.</li></ul>
|
||||
<p><code>approach_hint</code> must <strong>not</strong> override, synthesize, or fall back from an absent or invalid <code>harness_profile_ref</code> on governed execution. A missing profile is an error, never an invitation to guess from a hint. Once inventory shows no caller depends on <code>approach_hint</code> for runtime selection, that use is deprecated.</p>
|
||||
<h3>3. activity-core does not mirror the profile catalog</h3>
|
||||
<p>The catalog in glas-harness is authoritative. activity-core must not maintain an independent list, which would drift and produce a second, disagreeing opinion about what is executable.</p>
|
||||
<p>Consequence, accepted knowingly: Glas exposes deterministic validation through its package and CLI but <strong>no network validation service</strong> today, so activity-core cannot remotely validate a profile ref at emission time. We therefore validate only what is local and structural — that a ref is present and well-formed for governed execution — and rely on the <strong>execution-side Glas resolver as the mandatory fail-closed check</strong>.</p>
|
||||
<p>This is weaker than refusing at emission, and deliberately so: an unreachable or absent validation service must never downgrade into "emit anything". If emit-time remote validation is later required, it needs a separately scoped Glas API and its own decision — not a mirrored catalogue here.</p>
|
||||
<p>Even unvalidated at emit, this is strictly better than the 2026-08-17 failure: refusal happens deterministically before sandbox creation rather than after a claim and lease were consumed.</p>
|
||||
<h3>4. activity-core does not become an executor</h3>
|
||||
<p>We emit an authorized, profile-named request and record normalized evidence. We do not select reins, provision sandboxes, acquire credentials, or run any inner agentic loop. Glas owns profile resolution and the outer loop; the rein owns its inner loop; <code>sand-boxer</code> owns isolation. This is SCOPE drift risk #1 ("convenience execution") and this ADR does not relax it.</p>
|
||||
<p>The attribution references we carry (<code>assignment_ref</code>, <code>role_ref</code>, <code>duty_ref</code>, <code>goal_refs</code>, <code>resource_envelope_refs</code>) are <strong>passed through</strong>, not authored here. Their vocabulary belongs to <code>info-tech-canon</code>; activity-core must not invent org roles (ACTIVITY-WP-0029 responsibility map).</p>
|
||||
<p>ACT-ADR-007's bounded-operation exception does not alter this decision. A code-registered fixed maintenance operation is not a Glas/rein execution constellation, and the exception cannot be used to run an agent loop or execute an <code>ops_run</code> inside activity-core.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>ops_runs</code> grows <code>harness_profile_ref</code> and the attribution refs; the emission path, queue projection, and run artefacts carry them.</li><li>Definitions declare a profile; rules pass it through.</li><li>Unroutable requests fail before sandbox creation instead of at claim time.</li><li>activity-core is no longer coupled to one rein in practice.</li><li>Emit-time validation remains a known gap, owned by a future Glas API.</li><li>Implementation: <strong>ACTIVITY-WP-0032</strong>.</li></ul>
|
||||
</section><footer><span>ACT-ADR-006 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-006-glas-profile-execution.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ea6fb0e92a25825b42e00b2f85aa23cf50605294">
|
||||
<meta name="policy-source-digest" content="88041e9eb3a0f91fdd9da46f9813d7304b5ba58e62f9d28ea8f63c0a7b055b63">
|
||||
<title>Profile-driven execution selection over the ops_run pull queue</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-006</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-08-21</span><span>generated from canonical source — do not edit</span></div><h1>Profile-driven execution selection over the ops_run pull queue</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-006-glas-profile-execution.md · ea6fb0e92a25825b42e00b2f85aa23cf50605294</code></p><p class="sub">Review due: 2027-02-21</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted</strong> (2026-08-21). Hub decision <code>147beec6-7fe7-4837-8e3a-4264a240379d</code> ("Glas contract 1.0 makes execution constellation selection explicit").</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ACT-ADR-005 gave internal scheduled automation a claimable <code>ops_run</code> plane. What an ops_run said about <em>how</em> to execute was <code>approach_hint</code> — free text, interpreted by the claiming executor at claim time.</p>
|
||||
<p>That binding is too late. Live failure, railiance01 2026-08-17:</p>
|
||||
<pre>state=failed attempt=1 claim_owner=rein-aharness@railiance01
|
||||
title=Run SBOM rescan for binect-js
|
||||
failure=no approach matched labels/definition;
|
||||
labels=['sbom','security','automated']</pre>
|
||||
<p>The run was claimed and leased, <em>then</em> died because nothing could execute it. A claim was consumed to discover the request was unroutable.</p>
|
||||
<p><code>glas-harness</code> GLAS-WP-0004 closed contract 1.0 and proved it by running one bounded task through two different reins (<code>rein-aharness</code>, <code>rein-openweights</code>) via an explicit <code>harness_profile_ref</code>, with real commits and verified sandbox destruction. Meanwhile every activity-core claim in the week to 2026-08-21 was <code>rein-aharness@railiance01</code> — a coupling that is now avoidable rather than inherent.</p>
|
||||
<p>Two shapes were considered: keep the pull queue and carry the profile as payload, or have activity-core call the Glas gateway directly. glas-harness, which owns the contract, advised the first for the initial migration.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. The queue stays; the payload changes</h3>
|
||||
<p>activity-core continues to insert <code>ops_run(open)</code> and executors continue to claim under lease. Scheduling topology does not change. What changes is that a queued run carries an exact <strong>versioned <code>harness_profile_ref</code></strong> plus the correlation / assignment / role / duty / goal / resource-envelope references. The claiming executor passes that request into Glas, which resolves or refuses it <strong>before sandbox creation</strong>.</p>
|
||||
<p>This changes the execution contract without simultaneously changing the scheduling topology — one variable at a time. A direct activity-core → Glas call may be evaluated later, independently, if pull delivery stops meeting operational needs.</p>
|
||||
<h3>2. <code>harness_profile_ref</code> is authoritative; <code>approach_hint</code> is legacy</h3>
|
||||
<p>The two coexist with <strong>distinct semantics</strong>, not as fallbacks for one another:</p>
|
||||
<ul><li><code>harness_profile_ref</code> is the authoritative execution-constellation selector.</li><li><code>approach_hint</code> remains only a legacy activity/definition-matching hint while producers and consumers migrate.</li></ul>
|
||||
<p><code>approach_hint</code> must <strong>not</strong> override, synthesize, or fall back from an absent or invalid <code>harness_profile_ref</code> on governed execution. A missing profile is an error, never an invitation to guess from a hint. Once inventory shows no caller depends on <code>approach_hint</code> for runtime selection, that use is deprecated.</p>
|
||||
<h3>3. activity-core does not mirror the profile catalog</h3>
|
||||
<p>The catalog in glas-harness is authoritative. activity-core must not maintain an independent list, which would drift and produce a second, disagreeing opinion about what is executable.</p>
|
||||
<p>Consequence, accepted knowingly: Glas exposes deterministic validation through its package and CLI but <strong>no network validation service</strong> today, so activity-core cannot remotely validate a profile ref at emission time. We therefore validate only what is local and structural — that a ref is present and well-formed for governed execution — and rely on the <strong>execution-side Glas resolver as the mandatory fail-closed check</strong>.</p>
|
||||
<p>This is weaker than refusing at emission, and deliberately so: an unreachable or absent validation service must never downgrade into "emit anything". If emit-time remote validation is later required, it needs a separately scoped Glas API and its own decision — not a mirrored catalogue here.</p>
|
||||
<p>Even unvalidated at emit, this is strictly better than the 2026-08-17 failure: refusal happens deterministically before sandbox creation rather than after a claim and lease were consumed.</p>
|
||||
<h3>4. activity-core does not become an executor</h3>
|
||||
<p>We emit an authorized, profile-named request and record normalized evidence. We do not select reins, provision sandboxes, acquire credentials, or run any inner agentic loop. Glas owns profile resolution and the outer loop; the rein owns its inner loop; <code>sand-boxer</code> owns isolation. This is SCOPE drift risk #1 ("convenience execution") and this ADR does not relax it.</p>
|
||||
<p>The attribution references we carry (<code>assignment_ref</code>, <code>role_ref</code>, <code>duty_ref</code>, <code>goal_refs</code>, <code>resource_envelope_refs</code>) are <strong>passed through</strong>, not authored here. Their vocabulary belongs to <code>info-tech-canon</code>; activity-core must not invent org roles (ACTIVITY-WP-0029 responsibility map).</p>
|
||||
<p>ACT-ADR-007's bounded-operation exception does not alter this decision. A code-registered fixed maintenance operation is not a Glas/rein execution constellation, and the exception cannot be used to run an agent loop or execute an <code>ops_run</code> inside activity-core.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>ops_runs</code> grows <code>harness_profile_ref</code> and the attribution refs; the emission path, queue projection, and run artefacts carry them.</li><li>Definitions declare a profile; rules pass it through.</li><li>Unroutable requests fail before sandbox creation instead of at claim time.</li><li>activity-core is no longer coupled to one rein in practice.</li><li>Emit-time validation remains a known gap, owned by a future Glas API.</li><li>Implementation: <strong>ACTIVITY-WP-0032</strong>.</li></ul>
|
||||
</section><footer><span>ACT-ADR-006 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-006-glas-profile-execution.md · ea6fb0e92a25825b42e00b2f85aa23cf50605294</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
|
||||
<meta name="policy-source-digest" content="b9005e5f23dce53169e5f614ed461ce49e975266ff9a8fc01e2c6364fc5c91d3">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="984208262a0f6b67a6dc94cb078ebd0bcddc95d0b21a694d7e207677c10b471f">
|
||||
<title>Ops runs vs development work records — claim queue and plane split</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-005</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-08-03</span><span>generated from canonical source — do not edit</span></div><h1>Ops runs vs development work records — claim queue and plane split</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</code></p><p class="sub">Review due: 2027-02-03</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#topology"><span class="n">·</span>Topology</a></li><li><a href="#implementation-stack"><span class="n">·</span>Implementation stack</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-005</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-08-03</span><span>generated from canonical source — do not edit</span></div><h1>Ops runs vs development work records — claim queue and plane split</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2027-02-03</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#topology"><span class="n">·</span>Topology</a></li><li><a href="#implementation-stack"><span class="n">·</span>Implementation stack</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted</strong> (2026-08-03).</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -202,6 +202,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<div class="scroll"><table><thead><tr><th>Path</th><th>Failure</th></tr></thead><tbody><tr><td>Emit <code>activity_task_spawn</code> progress only</td><td>Append-only; <strong>not claimable</strong>; schedule can “succeed” while nothing runs</td></tr><tr><td><code>ISSUE_SINK_TYPE=rest</code> → issue-core → Forgejo</td><td>Spam + 503s; issue-core INTENT forbids origin of fleet work</td></tr><tr><td>Host systemd timers dual-clocked with Temporal</td><td>Shadow scheduler; weak self-healing</td></tr></tbody></table></div>
|
||||
<p>issue-core’s <strong>correct</strong> role is a <strong>connector facade</strong> over external trackers (Forgejo, GitHub, Jira, …) so agents need not know each backend. It is <strong>not</strong> the origin of work records and <strong>not</strong> the default internal ops queue. <strong>Gitea is out of scope</strong> for this fleet; the self-hosted forge is <strong>Forgejo</strong>.</p>
|
||||
<p>Canon already allows a DB-only exception for “runtime operations data (logs, metrics, run histories, token events)” (<code>work-record-types_v0.1.md</code>). This ADR names that exception for <strong>ops runs</strong>.</p>
|
||||
<p>This is also the precise meaning of INTENT's no-task-lifecycle boundary: <code>ops_run</code> claim/lease/outcome is durable runtime delivery state, not work-item state. It may not grow assignment, commitments, dependencies, project phases, or manually managed task status.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. Two planes for “work,” one vocabulary for people</h3>
|
||||
|
|
@ -210,8 +211,9 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<h3>2. activity-core owns the ops_run claim queue</h3>
|
||||
<p>When a definition emits a TaskSpec for internal fleet work:</p>
|
||||
<ol><li>Write <strong><code>ops_run</code></strong> row (durable, claimable) with idempotency key (e.g. <code>fi-daily:2026-08-04</code> for once-per-day briefs).</li><li>Optionally dual-write <code>activity_task_spawn</code> progress for existing observers.</li><li>Do <strong>not</strong> open a Forgejo issue.</li><li>Do <strong>not</strong> create a workplan task file for that day’s fire.</li></ol>
|
||||
<p>Claim API (sketch; implement in ACTIVITY-WP-0026):</p>
|
||||
<p>Claim API (implemented in ACTIVITY-WP-0026 and hardened in ACTIVITY-WP-0036):</p>
|
||||
<ul><li><code>POST /ops-runs/claim</code> — lease next open run matching labels / worker id</li><li><code>POST /ops-runs/{id}/complete</code> — succeeded + completion metadata</li><li><code>POST /ops-runs/{id}/fail</code> — failed + retry policy</li><li><code>GET /ops-runs?state=open</code> — operator visibility</li></ul>
|
||||
<p>Worker mutation credentials are bound to one configured queue <code>worker_id</code>; the request body cannot assert a different claim owner. Heartbeat, completion, and failure lock the row and require its lease deadline to remain strictly in the future. Operator/SSO credentials provide visibility and explicit administration, but do not act as a normal worker identity.</p>
|
||||
<p>activity-core remains <strong>when / what / where</strong> only: it does <strong>not</strong> run domain LLM sessions or hold tenant git credentials.</p>
|
||||
<h3>3. rein-aharness owns how (approach selection + execute)</h3>
|
||||
<ul><li>Continuous <strong>claim loop</strong> (service / Deployment), not dual wall-clock timers as source of truth.</li><li>Host systemd timers become <strong>break-glass</strong> after cutover.</li><li><strong>Approach selection</strong> at claim time (minimal table v1):</li></ul>
|
||||
|
|
@ -244,4 +246,4 @@ forgejo: optional external collab via issue-core projection only</pre>
|
|||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li><code>docs/recurring-automations-playbook.md</code></li><li><code>docs/task-emission-consumer-contract.md</code></li><li><code>docs/issue-core-emission-boundary.md</code></li><li>ACTIVITY-WP-0022, ACTIVITY-WP-0023 (G2)</li><li><code>the-custodian/canon/standards/work-record-types_v0.1.md</code></li><li><code>issue-core/INTENT.md</code> (work-record boundary)</li><li><code>state-hub/docs/cluster-operating-model.md</code> (coulombcore primary)</li></ul>
|
||||
</section><footer><span>ACT-ADR-005 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>
|
||||
</section><footer><span>ACT-ADR-005 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="984208262a0f6b67a6dc94cb078ebd0bcddc95d0b21a694d7e207677c10b471f">
|
||||
<title>Ops runs vs development work records — claim queue and plane split</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-005</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-08-03</span><span>generated from canonical source — do not edit</span></div><h1>Ops runs vs development work records — claim queue and plane split</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2027-02-03</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#topology"><span class="n">·</span>Topology</a></li><li><a href="#implementation-stack"><span class="n">·</span>Implementation stack</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted</strong> (2026-08-03).</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The fleet has two healthy patterns that were forced into one vague “task” idea:</p>
|
||||
<ol><li><strong>Development / coordination work</strong> — structured workplans and tasks as <strong>repo files</strong>, indexed by <strong>State Hub</strong> on <strong>coulombcore</strong> (<code>fix-consistency</code>, UUIDv7 write-back). This matches ADR-001 and <code>work-record-types_v0.1.md</code> and is how humans and coding agents ship product.</li></ol>
|
||||
<ol><li><strong>Operations / scheduled automation</strong> — activity-core Temporal schedules (when / what / where), rein-aharness execution (how), completion events (idempotence). Instances of “today’s FI brief” or “this prune fire” are <strong>ephemeral runs</strong>, not multi-day workplan bodies.</li></ol>
|
||||
<p>Practice collapsed (2) into broken paths:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Path</th><th>Failure</th></tr></thead><tbody><tr><td>Emit <code>activity_task_spawn</code> progress only</td><td>Append-only; <strong>not claimable</strong>; schedule can “succeed” while nothing runs</td></tr><tr><td><code>ISSUE_SINK_TYPE=rest</code> → issue-core → Forgejo</td><td>Spam + 503s; issue-core INTENT forbids origin of fleet work</td></tr><tr><td>Host systemd timers dual-clocked with Temporal</td><td>Shadow scheduler; weak self-healing</td></tr></tbody></table></div>
|
||||
<p>issue-core’s <strong>correct</strong> role is a <strong>connector facade</strong> over external trackers (Forgejo, GitHub, Jira, …) so agents need not know each backend. It is <strong>not</strong> the origin of work records and <strong>not</strong> the default internal ops queue. <strong>Gitea is out of scope</strong> for this fleet; the self-hosted forge is <strong>Forgejo</strong>.</p>
|
||||
<p>Canon already allows a DB-only exception for “runtime operations data (logs, metrics, run histories, token events)” (<code>work-record-types_v0.1.md</code>). This ADR names that exception for <strong>ops runs</strong>.</p>
|
||||
<p>This is also the precise meaning of INTENT's no-task-lifecycle boundary: <code>ops_run</code> claim/lease/outcome is durable runtime delivery state, not work-item state. It may not grow assignment, commitments, dependencies, project phases, or manually managed task status.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. Two planes for “work,” one vocabulary for people</h3>
|
||||
<div class="scroll"><table><thead><tr><th>Plane</th><th>Artefact</th><th>Home</th><th>Lifecycle</th></tr></thead><tbody><tr><td><strong>A — Development / coordination</strong></td><td>Work records (<code>workplan</code>, <code>task</code>, <code>intake</code>, …)</td><td>Repo files + State Hub (coulombcore)</td><td>File-backed, reviewable, multi-day</td></tr><tr><td><strong>B — Recurrence control</strong></td><td>Activity definitions + Temporal schedules</td><td>activity-core (railiance)</td><td>Git definitions; DB schedule state</td></tr><tr><td><strong>C — Ops execution</strong></td><td><strong><code>ops_run</code></strong> (runtime claim object)</td><td><strong>activity-core DB + claim API</strong></td><td>open → claimed → succeeded \</td><td>failed \</td><td>expired</td></tr><tr><td><strong>D — External collab</strong></td><td>Tracker issues</td><td><strong>issue-core</strong> optional projection to <strong>Forgejo</strong> (and others)</td><td>Never default for internal automation</td></tr></tbody></table></div>
|
||||
<p>Coding agents orient on <strong>Plane A</strong> via State Hub. Automation orients on <strong>Planes B+C</strong>. Humans outside the fleet may see <strong>Plane D</strong> only when deliberately projected.</p>
|
||||
<h3>2. activity-core owns the ops_run claim queue</h3>
|
||||
<p>When a definition emits a TaskSpec for internal fleet work:</p>
|
||||
<ol><li>Write <strong><code>ops_run</code></strong> row (durable, claimable) with idempotency key (e.g. <code>fi-daily:2026-08-04</code> for once-per-day briefs).</li><li>Optionally dual-write <code>activity_task_spawn</code> progress for existing observers.</li><li>Do <strong>not</strong> open a Forgejo issue.</li><li>Do <strong>not</strong> create a workplan task file for that day’s fire.</li></ol>
|
||||
<p>Claim API (implemented in ACTIVITY-WP-0026 and hardened in ACTIVITY-WP-0036):</p>
|
||||
<ul><li><code>POST /ops-runs/claim</code> — lease next open run matching labels / worker id</li><li><code>POST /ops-runs/{id}/complete</code> — succeeded + completion metadata</li><li><code>POST /ops-runs/{id}/fail</code> — failed + retry policy</li><li><code>GET /ops-runs?state=open</code> — operator visibility</li></ul>
|
||||
<p>Worker mutation credentials are bound to one configured queue <code>worker_id</code>; the request body cannot assert a different claim owner. Heartbeat, completion, and failure lock the row and require its lease deadline to remain strictly in the future. Operator/SSO credentials provide visibility and explicit administration, but do not act as a normal worker identity.</p>
|
||||
<p>activity-core remains <strong>when / what / where</strong> only: it does <strong>not</strong> run domain LLM sessions or hold tenant git credentials.</p>
|
||||
<h3>3. rein-aharness owns how (approach selection + execute)</h3>
|
||||
<ul><li>Continuous <strong>claim loop</strong> (service / Deployment), not dual wall-clock timers as source of truth.</li><li>Host systemd timers become <strong>break-glass</strong> after cutover.</li><li><strong>Approach selection</strong> at claim time (minimal table v1):</li></ul>
|
||||
<div class="scroll"><table><thead><tr><th>Labels / definition family</th><th>Approach</th></tr></thead><tbody><tr><td>mail / mail-intake</td><td>deterministic adapter (+ optional triage)</td></tr><tr><td>research-brief / fi-daily</td><td>structured llm-connect (<code>fi-research-brief</code>)</td></tr><tr><td>binky rhythm</td><td>structured llm-connect (<code>brief-daily</code>)</td></tr><tr><td>agent-session</td><td>persona + tool profile session</td></tr><tr><td>unknown</td><td>refuse; do not invent</td></tr></tbody></table></div>
|
||||
<ul><li>Success posts <strong>domain completion event</strong> (e.g. <code>fi_daily_brief</code>) so resolvers set <code>due=false</code>, and closes the <code>ops_run</code>.</li></ul>
|
||||
<h3>4. State Hub remains the work-record read model</h3>
|
||||
<ul><li>Stays on <strong>coulombcore</strong>; railiance peers via edge relay / ops-bridge.</li><li>May <strong>project</strong> open/failed ops runs for fleet ops UI (read-only), but is <strong>not</strong> the claim authority.</li><li>Development workplans/tasks continue file + <code>fix-consistency</code> only.</li></ul>
|
||||
<h3>5. issue-core is Forgejo (and multi-tracker) facade only</h3>
|
||||
<ul><li><strong>Forgejo</strong> is the self-hosted forge; do not plan for <strong>Gitea</strong> as a product.</li><li>Optional <strong>project / link</strong> of an existing work-record UUID to an external issue when collaboration needs it.</li><li>Authenticated <code>POST /issues/</code> means “create/link <strong>external</strong> tracker work,” never “spawn fleet automation.”</li><li>Internal automation <strong>must not</strong> use issue-core as the ops claim queue unless a future design adds an <strong>internal-only</strong> backend with projection <strong>hard-off</strong> for automation labels — out of scope unless a later ADR says otherwise.</li></ul>
|
||||
<h3>6. Promotion path ops → dev</h3>
|
||||
<p>When an ops run discovers multi-day product work (e.g. “collect Kimi K3 under new quota”), the executor or human <strong>promotes</strong> an <code>intake</code> / workplan in the domain repo (Plane A). Ops runs never become permanent fake workplans.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<h3>Positive</h3>
|
||||
<ul><li>Self-healing recurrence: schedule → claimable row → claim → complete/fail.</li><li>Aligns issue-core INTENT with practice; stops Forgejo spam path.</li><li>Keeps activity-core thin; keeps rein-aharness as sole session/credential shell.</li><li>Preserves the good dev loop (files + State Hub).</li></ul>
|
||||
<h3>Negative / cost</h3>
|
||||
<ul><li>New schema + API + migrator in activity-core.</li><li>rein-aharness must leave issue-core-only poll as primary for ops.</li><li>Temporary dual-write and dual timers until cutover proven.</li></ul>
|
||||
<h3>Non-goals</h3>
|
||||
<ul><li>Replacing workplans for development.</li><li>Running domain briefs inside activity-core workers.</li><li>Global <code>ISSUE_SINK_TYPE=rest</code> to Forgejo.</li><li>Gitea support or migration paths.</li></ul>
|
||||
</section>
|
||||
<section id="topology"><h2>Topology</h2>
|
||||
<pre>coulombcore: State Hub + work-record registry
|
||||
railiance: activity-core (Temporal + ops_run queue)
|
||||
rein-aharness (claim loop + approach + llm-connect)
|
||||
domain checkouts
|
||||
forgejo: optional external collab via issue-core projection only</pre>
|
||||
</section>
|
||||
<section id="implementation-stack"><h2>Implementation stack</h2>
|
||||
<div class="scroll"><table><thead><tr><th>Workplan</th><th>Owner</th><th>Role</th></tr></thead><tbody><tr><td>ACTIVITY-WP-0026</td><td>activity-core</td><td>ops_run schema, claim API, emit path, dual-write</td></tr><tr><td>REIN-A-0002</td><td>rein-aharness</td><td>claim loop, approach table, FI/Binky cutover</td></tr><tr><td>ISSUE-WP-0006</td><td>issue-core</td><td>Forgejo-only language; projection boundary; no ops queue</td></tr><tr><td>STATE-WP-0078</td><td>state-hub</td><td>Read projection of ops_run for ops UI (optional consume)</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li><code>docs/recurring-automations-playbook.md</code></li><li><code>docs/task-emission-consumer-contract.md</code></li><li><code>docs/issue-core-emission-boundary.md</code></li><li>ACTIVITY-WP-0022, ACTIVITY-WP-0023 (G2)</li><li><code>the-custodian/canon/standards/work-record-types_v0.1.md</code></li><li><code>issue-core/INTENT.md</code> (work-record boundary)</li><li><code>state-hub/docs/cluster-operating-model.md</code> (coulombcore primary)</li></ul>
|
||||
</section><footer><span>ACT-ADR-005 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
|
||||
<meta name="policy-source-digest" content="89b3a925d8cf6d9dbfe426980021b58281350201f316654b7fc1ee6554910ac6">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="92da58f58ee3dbb62233161a0fc0b7780920f81ec37fdd40d476be354f24e0eb">
|
||||
<title>The Producer Trust Boundary — Guardrails and Error-Correction for Untrusted Output</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-004</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-06-26</span><span>generated from canonical source — do not edit</span></div><h1>The Producer Trust Boundary — Guardrails and Error-Correction for Untrusted Output</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-004-producer-trust-boundary.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</code></p><p class="sub">Review due: 2026-12-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-004</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-06-26</span><span>generated from canonical source — do not edit</span></div><h1>The Producer Trust Boundary — Guardrails and Error-Correction for Untrusted Output</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-004-producer-trust-boundary.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-12-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -209,7 +209,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<ol><li><strong>Push verification to the boundary; keep the interior strict.</strong> Apply posture <strong>B</strong> at the producer→consumer boundary; keep posture <strong>A</strong> for residual exceptions inside the verified core. Never relax the interior schema to absorb producer sloppiness.</li><li><strong>Make error locality match the unit of work.</strong> One bad recommendation must cost one recommendation, not the whole report. Structuring the payload so each item is independently parseable and validatable is the highest-leverage change.</li><li><strong>Quarantine, never silently drop.</strong> Invalid units are preserved as bounded, provenance-tagged artifacts (<code>index</code>, <code>error</code>, <code>raw</code> snippet, <code>reason</code>) so they can be debugged or replayed. Degraded-but-usable is reported distinctly from total loss.</li><li><strong>Both human and agent input get the same rigor.</strong> Guardrails are producer-agnostic: the same count / length / depth caps and reference allow-lists apply whether the producer is an LLM, an agent, or a human.</li></ol>
|
||||
<h3>What this means concretely in activity-core</h3>
|
||||
<p>Implemented in <code>src/activity_core/rules/executor.py</code>:</p>
|
||||
<ul><li><strong>Strict-structure-only schema.</strong> The daily-triage output schema is strict on per-item <em>structure</em> (<code>required [rank, candidate, action, why]</code>, typed <code>wsjf</code>) and carries <code>maxItems</code> as a producer <em>hint</em> — never as a hard whole-document reject, which would reproduce the very blast-radius failure (ACT-ADR-002 governs the schema format; <code>schemas/daily-triage-report.json</code>).</li><li><strong>Item-granular recovery (posture B).</strong> When whole-document parse + one retry fail, <code>_resilient_report</code> recovers individually-parseable recommendation objects via a brace/quote-aware scanner (<code>_extract_object_spans</code>) that works for both pretty-printed and NDJSON output, attempts a best-effort <code>_try_repair</code> on a truncated tail, validates each recovered object against the item schema, and keeps the valid ones. Survivors are emitted with <code>output_validated=true</code>, <code>partial=true</code>, and <code>review_required=true</code>.</li><li><strong>Producer guardrails (<code>_partition_items</code>, applied on both the recovery and the happy path).</strong> Per recommendation: structural type → schema → structural caps (<code>_MAX_DEPTH</code>, <code>_MAX_STRING_LEN</code>) → reference allow-list → count cap (top-N by <code>maxItems</code>). The first failing check quarantines the item with provenance and a <code>reason</code> (<code>malformed</code> / <code>schema</code> / <code>guardrail</code> / <code>allow_list</code> / <code>over_limit</code>).</li><li><strong>Reference allow-list.</strong> A recommendation whose <code>candidate</code> is not in the set of known ids is quarantined. The set is sourced from resolved context (<code>context["known_candidates"]</code>, via <code>_allow_list_from_context</code>); the check is inert until a context resolver populates it, so the capability ships now and activates with a one-line resolver change.</li></ul>
|
||||
<ul><li><strong>Strict-structure-only schema.</strong> The daily-triage output schema is strict on per-item <em>structure</em> (<code>required [rank, candidate, action, why]</code>, typed <code>wsjf</code>) and carries <code>maxItems</code> as a producer <em>hint</em> — never as a hard whole-document reject, which would reproduce the very blast-radius failure (ACT-ADR-002 governs the schema format; <code>schemas/daily-triage-report.json</code>).</li><li><strong>Item-granular recovery (posture B).</strong> When whole-document parse + one retry fail, <code>_resilient_report</code> recovers individually-parseable recommendation objects via a brace/quote-aware scanner (<code>_extract_object_spans</code>) that works for both pretty-printed and NDJSON output, attempts a best-effort <code>_try_repair</code> on a truncated tail, validates each recovered object against the item schema, and keeps the valid ones. Survivors are emitted with <code>output_validated=true</code>, <code>partial=true</code>, and <code>review_advisory=true</code> (<code>review_gate_applied=false</code>).</li><li><strong>Producer guardrails (<code>_partition_items</code>, applied on both the recovery and the happy path).</strong> Per recommendation: structural type → schema → structural caps (<code>_MAX_DEPTH</code>, <code>_MAX_STRING_LEN</code>) → reference allow-list → count cap (top-N by <code>maxItems</code>). The first failing check quarantines the item with provenance and a <code>reason</code> (<code>malformed</code> / <code>schema</code> / <code>guardrail</code> / <code>allow_list</code> / <code>over_limit</code>).</li><li><strong>Reference allow-list.</strong> A recommendation whose <code>candidate</code> is not in the set of known ids is quarantined. The set is sourced from resolved context (<code>context["known_candidates"]</code>, via <code>_allow_list_from_context</code>); the check is inert until a context resolver populates it, so the capability ships now and activates with a one-line resolver change.</li></ul>
|
||||
<h3>Where each posture sits</h3>
|
||||
<div class="scroll"><table><thead><tr><th>Layer</th><th>Posture</th><th>Mechanism</th></tr></thead><tbody><tr><td>Schema / contract</td><td>B</td><td>strict per-item structure; <code>maxItems</code> as hint</td></tr><tr><td>Whole-document parse</td><td>A</td><td>tolerant parse + single retry</td></tr><tr><td>Failed parse</td><td>B</td><td>item-granular recovery + repair + quarantine</td></tr><tr><td>Per-item screening</td><td>B</td><td>schema + depth/length caps + allow-list + count cap</td></tr><tr><td>Emitted report</td><td>—</td><td><code>partial</code> / <code>quarantined_*</code> provenance; never silent</td></tr></tbody></table></div>
|
||||
</section>
|
||||
|
|
@ -221,4 +221,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li>ACT-ADR-002 — markdown-as-definition format and output schema governance.</li><li>ACT-ADR-003 — Rule vs. Instruction model; the Instruction prompt-injection surface this boundary complements on the output side.</li><li><code>workplans/ACTIVITY-WP-0016-llm-output-robustness-trust-boundary.md</code> — the implementing workplan.</li></ul>
|
||||
</section><footer><span>ACT-ADR-004 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-004-producer-trust-boundary.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>
|
||||
</section><footer><span>ACT-ADR-004 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-004-producer-trust-boundary.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,224 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="92da58f58ee3dbb62233161a0fc0b7780920f81ec37fdd40d476be354f24e0eb">
|
||||
<title>The Producer Trust Boundary — Guardrails and Error-Correction for Untrusted Output</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-004</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-06-26</span><span>generated from canonical source — do not edit</span></div><h1>The Producer Trust Boundary — Guardrails and Error-Correction for Untrusted Output</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-004-producer-trust-boundary.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-12-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>On 2026-06-26 the scheduled daily WSJF triage instruction fired on time, called llm-connect successfully, and produced a long ranked recommendation list — but the JSON broke at char 5268 (~rank 8–9 of ~16), failing schema validation. Because the report was validated and consumed as a single monolithic JSON document, one malformed delimiter discarded the <strong>entire</strong> run, including the 7 perfectly good recommendations the model had already emitted. The scheduling and runtime layers were healthy; the failure was entirely at the seam where free-form model output meets a strict consumer.</p>
|
||||
<p>This is not a one-off bug, it is a recurring class. activity-core has a <strong>trust boundary</strong> wherever generative or human-authored output meets strict deterministic consumers: the JSON Schema validator, the task emitter, and any classic compute pipeline downstream. The producers on the other side of that boundary — <strong>LLMs, agents, and humans</strong> — are all <em>untrusted producers</em>. Their output may be:</p>
|
||||
<ul><li><strong>erroneous</strong> — hallucination, truncation at a token limit, drift, type slips, typos, a missing delimiter; or</li><li><strong>malicious</strong> — prompt injection, crafted payloads, or oversized / deeply-nested structures intended to exhaust or confuse the consumer.</li></ul>
|
||||
<p>The pre-existing design treated producer output optimistically: parse the whole document, validate the whole document, and on any failure discard the whole document (preserving only a bounded diagnostic preview). That gives <strong>zero error locality</strong> — the blast radius of any single defect is the entire activation.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Treat the producer→consumer seam as an explicit, adversarial <strong>trust boundary</strong>, and place guardrails plus error-correction tooling <em>at that boundary</em> rather than letting raw producer output flow into deterministic consumers.</p>
|
||||
<h3>Two non-fail-fast postures</h3>
|
||||
<p>When hard-failing on a problem is undesirable, there are two sound strategies, and they <strong>compose</strong>:</p>
|
||||
<ul><li><strong>A) Trust but handle exceptions</strong> (optimistic / reactive). Consume the output as-is; on exception, catch → repair → retry → or quarantine. Cheap on the happy path; blast radius depends entirely on how granular the catch is. Best when failures are rare and locally recoverable. Risk: failures surface late, possibly after partial side effects.</li><li><strong>B) Verify and mitigate</strong> (defensive / proactive). Validate, sanitize, clamp, and normalize the output to a known-good shape <em>before</em> it enters the pipeline — drop bad items, coerce types, bound sizes/depth, allow-list references — so the consumer only ever sees clean input. Higher upfront cost, smaller blast radius, no partial side effects. Best when failures are common or consequences are high.</li></ul>
|
||||
<h3>Governing principles</h3>
|
||||
<ol><li><strong>Push verification to the boundary; keep the interior strict.</strong> Apply posture <strong>B</strong> at the producer→consumer boundary; keep posture <strong>A</strong> for residual exceptions inside the verified core. Never relax the interior schema to absorb producer sloppiness.</li><li><strong>Make error locality match the unit of work.</strong> One bad recommendation must cost one recommendation, not the whole report. Structuring the payload so each item is independently parseable and validatable is the highest-leverage change.</li><li><strong>Quarantine, never silently drop.</strong> Invalid units are preserved as bounded, provenance-tagged artifacts (<code>index</code>, <code>error</code>, <code>raw</code> snippet, <code>reason</code>) so they can be debugged or replayed. Degraded-but-usable is reported distinctly from total loss.</li><li><strong>Both human and agent input get the same rigor.</strong> Guardrails are producer-agnostic: the same count / length / depth caps and reference allow-lists apply whether the producer is an LLM, an agent, or a human.</li></ol>
|
||||
<h3>What this means concretely in activity-core</h3>
|
||||
<p>Implemented in <code>src/activity_core/rules/executor.py</code>:</p>
|
||||
<ul><li><strong>Strict-structure-only schema.</strong> The daily-triage output schema is strict on per-item <em>structure</em> (<code>required [rank, candidate, action, why]</code>, typed <code>wsjf</code>) and carries <code>maxItems</code> as a producer <em>hint</em> — never as a hard whole-document reject, which would reproduce the very blast-radius failure (ACT-ADR-002 governs the schema format; <code>schemas/daily-triage-report.json</code>).</li><li><strong>Item-granular recovery (posture B).</strong> When whole-document parse + one retry fail, <code>_resilient_report</code> recovers individually-parseable recommendation objects via a brace/quote-aware scanner (<code>_extract_object_spans</code>) that works for both pretty-printed and NDJSON output, attempts a best-effort <code>_try_repair</code> on a truncated tail, validates each recovered object against the item schema, and keeps the valid ones. Survivors are emitted with <code>output_validated=true</code>, <code>partial=true</code>, and <code>review_advisory=true</code> (<code>review_gate_applied=false</code>).</li><li><strong>Producer guardrails (<code>_partition_items</code>, applied on both the recovery and the happy path).</strong> Per recommendation: structural type → schema → structural caps (<code>_MAX_DEPTH</code>, <code>_MAX_STRING_LEN</code>) → reference allow-list → count cap (top-N by <code>maxItems</code>). The first failing check quarantines the item with provenance and a <code>reason</code> (<code>malformed</code> / <code>schema</code> / <code>guardrail</code> / <code>allow_list</code> / <code>over_limit</code>).</li><li><strong>Reference allow-list.</strong> A recommendation whose <code>candidate</code> is not in the set of known ids is quarantined. The set is sourced from resolved context (<code>context["known_candidates"]</code>, via <code>_allow_list_from_context</code>); the check is inert until a context resolver populates it, so the capability ships now and activates with a one-line resolver change.</li></ul>
|
||||
<h3>Where each posture sits</h3>
|
||||
<div class="scroll"><table><thead><tr><th>Layer</th><th>Posture</th><th>Mechanism</th></tr></thead><tbody><tr><td>Schema / contract</td><td>B</td><td>strict per-item structure; <code>maxItems</code> as hint</td></tr><tr><td>Whole-document parse</td><td>A</td><td>tolerant parse + single retry</td></tr><tr><td>Failed parse</td><td>B</td><td>item-granular recovery + repair + quarantine</td></tr><tr><td>Per-item screening</td><td>B</td><td>schema + depth/length caps + allow-list + count cap</td></tr><tr><td>Emitted report</td><td>—</td><td><code>partial</code> / <code>quarantined_*</code> provenance; never silent</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>A single malformed or oversized item no longer discards an entire activation; the daily-triage run that failed on 2026-06-26 would now deliver its 7 valid recommendations and quarantine the broken tail.</li><li>Reports gain a <code>partial</code> / <code>quarantined_*</code> vocabulary; downstream report sinks and reviewers can distinguish degraded-but-usable from total loss.</li><li>Guardrail thresholds (<code>_MAX_DEPTH</code>, <code>_MAX_STRING_LEN</code>, <code>maxItems</code>, the allow-list) are policy knobs that will need tuning; they are intentionally conservative defaults, not a finished calibration.</li><li><strong>Known retention gap (follow-on):</strong> <code>LLMConnectClient.complete()</code> still returns only <code>content</code>, discarding <code>finish_reason</code>/<code>usage</code>, and the total-loss artifact caps raw output below realistic break points. Capturing those signals so failures stay debuggable is tracked as a retention fix, not closed by this ADR.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives considered</h2>
|
||||
<ul><li><strong>Hard-enforce <code>maxItems</code> in the validator.</strong> Rejected: a hard reject of an over-count document reproduces the whole-document blast radius. Mitigation (keep top-N, quarantine the rest) is preferred.</li><li><strong>Relax the schema to accept anything.</strong> Rejected: violates principle 1; pushes malformed data into downstream consumers.</li><li><strong>Retry-until-valid only (pure posture A).</strong> Rejected as the sole strategy: the 2026-06-26 failure recurred across both the initial attempt and the retry, so retry alone does not bound the blast radius.</li></ul>
|
||||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li>ACT-ADR-002 — markdown-as-definition format and output schema governance.</li><li>ACT-ADR-003 — Rule vs. Instruction model; the Instruction prompt-injection surface this boundary complements on the output side.</li><li><code>workplans/ACTIVITY-WP-0016-llm-output-robustness-trust-boundary.md</code> — the implementing workplan.</li></ul>
|
||||
</section><footer><span>ACT-ADR-004 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-004-producer-trust-boundary.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
|
||||
<meta name="policy-source-digest" content="81ccfde427525f9a8d47f93c346813bc2d4003a990b503343e27c842a81a2ea6">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="f1017730074727bcb0c68309c445ffb219ef647c8c5604adbfff76c4aa3f9d66">
|
||||
<title>Rule vs. Instruction Model and Expression DSL</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-003</span> <span class="stat">accepted · accepted-1</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Rule vs. Instruction Model and Expression DSL</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-003-rule-instruction-model.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-003</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Rule vs. Instruction Model and Expression DSL</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-003-rule-instruction-model.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -250,7 +250,7 @@ trusted_fields: # REQUIRED — explicit allowlist of payload
|
|||
- event.attributes.domain
|
||||
- event.attributes.tags
|
||||
model: claude-sonnet-4-6
|
||||
review_required: false # true | false — curator gate for output
|
||||
review_advisory: false # true | false — advisory evidence, not a gate
|
||||
prompt: |
|
||||
{prompt template — only trusted_fields may be interpolated}
|
||||
output_schema: {path to JSON schema file}</pre>
|
||||
|
|
@ -261,16 +261,16 @@ output_schema: {path to JSON schema file}</pre>
|
|||
<h3>Output schema enforcement</h3>
|
||||
<p>The LLM response is validated against <code>output_schema</code> using JSON Schema validation. If validation fails, the instruction retries once with the schema error appended to the prompt. If the second attempt also fails, the instruction records an <code>instruction_output_error</code> audit event and emits no tasks. Tasks are <strong>never created from unvalidated output</strong>.</p>
|
||||
<p>Structured output mode (tool_use / JSON mode) is used where the model supports it. The output schema must define <code>List[TaskSpec]</code> or a compatible envelope.</p>
|
||||
<h3><code>review_required: true</code></h3>
|
||||
<p>When set today, the instruction's task/report output is marked with <code>review_required=true</code> in activity-core audit metadata. For report-producing instructions, this flag is also persisted in configured report sinks so an operator can distinguish validated-but-review-worthy output from routine output.</p>
|
||||
<p>activity-core does <strong>not</strong> currently route proposed tasks to a pending review queue. That queue must be owned by issue-core, because issue-core owns task lifecycle state. Until issue-core exposes a review contract, <code>review_required</code> is metadata only; it must not be treated as evidence that live task creation was held for approval.</p>
|
||||
<p>Future issue-core review integration may use the same field, but that change must update the issue sink contract and tests before any ActivityDefinition relies on queue routing.</p>
|
||||
<h3><code>review_advisory: true</code></h3>
|
||||
<p>ACTIVITY-WP-0035 selected advisory-only semantics because no downstream owner currently exposes an acknowledged proposal/decision/release contract. The instruction's task/report evidence is marked <code>review_advisory=true</code> and <code>review_gate_applied=false</code> so an operator can distinguish review-worthy output without interpreting it as held for approval.</p>
|
||||
<p>The legacy input name <code>review_required</code> is accepted during migration and normalized to <code>review_advisory</code>; new definitions must not use it. activity-core does <strong>not</strong> route proposals to a pending-review queue and does not own review lifecycle state. A future hold/release design requires a named downstream owner, an idempotent release reference, an ADR update, and fail-closed emission tests.</p>
|
||||
<h3>Evaluation semantics</h3>
|
||||
<ul><li>Instructions are evaluated <strong>after</strong> all rules in the ActivityDefinition.</li><li>The optional <code>condition</code> field on an instruction uses the same Rule DSL as a first-pass filter — if the condition is false, the LLM is not called. This avoids LLM cost for events that clearly do not need instruction judgement.</li><li>Instructions are <strong>not</strong> first-match-only; all instructions whose conditions pass fire. An ActivityDefinition may have zero instructions.</li></ul>
|
||||
<h3>Audit trail</h3>
|
||||
<p>Every task emission records:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Field</th><th>Rule</th><th>Instruction</th></tr></thead><tbody><tr><td><code>source_type</code></td><td><code>"rule"</code></td><td><code>"instruction"</code></td></tr><tr><td><code>source_id</code></td><td>rule <code>id</code> from definition</td><td>instruction <code>id</code> from definition</td></tr><tr><td><code>source_version</code></td><td>ActivityDefinition version</td><td>ActivityDefinition version</td></tr><tr><td><code>triggering_event_id</code></td><td>event UUID</td><td>event UUID</td></tr><tr><td><code>condition_matched</code></td><td>expression string</td><td>expression string (pre-filter)</td></tr><tr><td><code>prompt_hash</code></td><td>—</td><td>SHA-256 of rendered prompt</td></tr><tr><td><code>model</code></td><td>—</td><td>model ID used</td></tr><tr><td><code>output_validated</code></td><td>—</td><td><code>true</code> / <code>false</code></td></tr><tr><td><code>review_required</code></td><td>—</td><td><code>true</code> / <code>false</code></td></tr></tbody></table></div>
|
||||
<div class="scroll"><table><thead><tr><th>Field</th><th>Rule</th><th>Instruction</th></tr></thead><tbody><tr><td><code>source_type</code></td><td><code>"rule"</code></td><td><code>"instruction"</code></td></tr><tr><td><code>source_id</code></td><td>rule <code>id</code> from definition</td><td>instruction <code>id</code> from definition</td></tr><tr><td><code>source_version</code></td><td>ActivityDefinition version</td><td>ActivityDefinition version</td></tr><tr><td><code>triggering_event_id</code></td><td>event UUID</td><td>event UUID</td></tr><tr><td><code>condition_matched</code></td><td>expression string</td><td>expression string (pre-filter)</td></tr><tr><td><code>prompt_hash</code></td><td>—</td><td>SHA-256 of rendered prompt</td></tr><tr><td><code>model</code></td><td>—</td><td>model ID used</td></tr><tr><td><code>output_validated</code></td><td>—</td><td><code>true</code> / <code>false</code></td></tr><tr><td><code>review_advisory</code></td><td>—</td><td><code>true</code> / <code>false</code>; no gate applied</td></tr></tbody></table></div>
|
||||
<p>The audit trail is written to the <code>task_spawn_log</code> table in activity-core's database and referenced from the task record in issue-core.</p>
|
||||
<p>The rendered prompt and provider response are deliberately not persisted. The prompt hash proves equality when an authorized operator can reconstruct the same input, but the audit contract does not promise reconstruction after source definitions or upstream event retention have changed. <code>activity_runs</code> retains the definition version and bounded context snapshot; reports may retain allowlisted route/usage metadata. Prompts, messages, tool output, credential fields, and provider blobs are excluded from run, progress, and public API evidence.</p>
|
||||
<h3>Testing strategy</h3>
|
||||
<p><strong>Rules</strong>: every rule can and should be unit-tested with fixture event payloads. A test helper <code>evaluate_rule(condition_str, event_fixture)</code> returns <code>bool</code> and raises on syntax errors. Tests live alongside ActivityDefinition files: <code>activity-definitions/{slug}.test.json</code> — a list of <code>{event, expected_rules_fired}</code> fixtures.</p>
|
||||
<p><strong>Instructions</strong>: instructions cannot be deterministically unit-tested. Instead:</p>
|
||||
|
|
@ -291,4 +291,4 @@ output_schema: {path to JSON schema file}</pre>
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ACT-ADR-001 — Event Bridge Architecture</li><li>ACT-ADR-002 — Definition format (where rule/instruction blocks live)</li><li>CUST-TFE-SCOPE-2026-000001 — task-flow-engine extraction (analogue pattern)</li><li><code>src/activity_core/rules/</code> — implementation home</li></ul>
|
||||
</section><footer><span>ACT-ADR-003 · accepted-1 · accepted</span><span>activity-core · docs/adr/adr-003-rule-instruction-model.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>
|
||||
</section><footer><span>ACT-ADR-003 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-003-rule-instruction-model.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="b72fdb5452bff51a867a0316edb994723b35f268">
|
||||
<meta name="policy-source-digest" content="f1017730074727bcb0c68309c445ffb219ef647c8c5604adbfff76c4aa3f9d66">
|
||||
<title>Rule vs. Instruction Model and Expression DSL</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ACT-ADR-003</span> <span class="stat">accepted · accepted-2</span> <span>activity-core</span> <span>reviewed 2026-05-14</span><span>generated from canonical source — do not edit</span></div><h1>Rule vs. Instruction Model and Expression DSL</h1><p class="sub">Source: <code>activity-core · docs/adr/adr-003-rule-instruction-model.md · b72fdb5452bff51a867a0316edb994723b35f268</code></p><p class="sub">Review due: 2026-11-14</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ActivityDefinitions need two distinct evaluation modes to cover the full range of automation scenarios in the Coulomb org:</p>
|
||||
<p><strong>Deterministic cases</strong>: "if this repo has tag <code>python-service</code> AND has no SBOM in the last 30 days, create a scan task." The condition is fully expressible as a boolean predicate over known attributes. The output is fixed by the template. No ambiguity, no LLM required, fully testable.</p>
|
||||
<p><strong>Judgement cases</strong>: "a new repository has been registered — based on its domain and profile, determine what domain-specific onboarding tasks are appropriate." The right answer depends on context that is expensive to encode as explicit rules. An LLM is a better evaluator than a rule tree, but introduces non-determinism, cost, and a new attack surface (prompt injection via event payload).</p>
|
||||
<p>Conflating these two modes into one mechanism produces a system that is either too rigid (rules only) or too unpredictable (LLM everywhere). The two modes need different evaluation pipelines, testing strategies, and audit trails.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>Two named, distinct evaluation modes: Rule and Instruction.</strong></p>
|
||||
<p>Terminology is deliberate. A <strong>Rule</strong> is deterministic and mechanical — it applies or it does not. An <strong>Instruction</strong> is contextual and interpretive — it guides an LLM agent to make a judgement call. Both are expressed as fenced blocks in ActivityDefinition markdown files (see ACT-ADR-002).</p>
|
||||
<h3>Rules</h3>
|
||||
<p>A Rule has two parts: a <strong>condition</strong> (boolean predicate) and one or more <strong>actions</strong> (task template references).</p>
|
||||
<h3>Condition expression language</h3>
|
||||
<p>The condition is a single-line string expression evaluated by a sandboxed AST walker — never <code>exec()</code> or <code>eval()</code>. The evaluator walks the parsed AST and whitelist-checks every node type before executing. Unknown node types raise an <code>UnsafeExpression</code> error at parse time, not at evaluation time.</p>
|
||||
<p><strong>Available operations</strong>:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Category</th><th>Syntax</th><th>Example</th></tr></thead><tbody><tr><td>Equality</td><td><code>==</code>, <code>!=</code></td><td><code>event.type == "org.repo.registered"</code></td></tr><tr><td>Comparison</td><td><code>></code>, <code><</code>, <code>>=</code>, <code><=</code></td><td><code>event.attributes.sbom_age_days > 30</code></td></tr><tr><td>Membership</td><td><code>in</code>, <code>not in</code></td><td><code>"python-service" in event.attributes.tags</code></td></tr><tr><td>Boolean</td><td><code>and</code>, <code>or</code>, <code>not</code></td><td><code>a and (b or not c)</code></td></tr><tr><td>Grouping</td><td><code>( )</code></td><td><code>(a or b) and c</code></td></tr><tr><td>Length</td><td><code>len(x)</code></td><td><code>len(event.attributes.affected_repos) > 0</code></td></tr><tr><td>Existence</td><td><code>x is None</code>, <code>x is not None</code></td><td><code>event.attributes.domain is not None</code></td></tr></tbody></table></div>
|
||||
<p><strong>Attribute access</strong> follows dot notation on the <code>event</code> object and the <code>context</code> object (populated by context sources declared in the ActivityDefinition):</p>
|
||||
<ul><li><code>event.id</code> — UUID string</li><li><code>event.type</code> — event type identifier</li><li><code>event.version</code> — event type version</li><li><code>event.timestamp</code> — ISO 8601 datetime string</li><li><code>event.publisher</code> — publisher identifier</li><li><code>event.attributes.{name}</code> — typed attribute per event type schema</li><li><code>context.{source}.{field}</code> — resolved context data</li></ul>
|
||||
<p><strong>Explicitly forbidden</strong> (evaluator rejects at parse time):</p>
|
||||
<ul><li>Function calls other than <code>len()</code> and <code>None</code> tests</li><li>Attribute access on arbitrary Python objects</li><li>String interpolation or formatting</li><li>Any control flow (<code>if</code>, <code>for</code>, <code>while</code>, <code>lambda</code>)</li><li>Import statements</li><li>Assignments</li></ul>
|
||||
<p><strong>Design rationale</strong>: the expression language is intentionally small. Anything complex enough to need more than this belongs in an Instruction, not a Rule. When a rule condition becomes difficult to express, that is a signal that the case requires LLM judgement, not a signal that the DSL needs more features.</p>
|
||||
<h3>Actions</h3>
|
||||
<p>A Rule's action block specifies:</p>
|
||||
<pre>action:
|
||||
task_template: "Run SBOM rescan for {context.repo.repo_slug}"
|
||||
target_repo: context.repo.repo_slug
|
||||
priority: medium
|
||||
labels: ["sbom", "security", "{context.repo.repo_slug}"]
|
||||
due_in_days: 7</pre>
|
||||
<p><code>action.task_template</code> is the emitted task title template. It is not a path to a repo-local file. Older design notes and the legacy <code>tasks/*.md</code> directory use "task template" for materialized task-body templates; that is a separate legacy surface. To avoid surprise, new rule actions should treat <code>task_template</code> as <code>title_template</code> semantics until the field can be renamed in a schema-breaking revision.</p>
|
||||
<p>Action fields accept two deterministic rendering forms:</p>
|
||||
<ul><li>Whole-field paths: if the whole string is a path like <code>context.repo.repo_slug</code> or <code>event.attributes.repo_slug</code>, the rendered value keeps the original scalar/list/object shape from that path. This is the correct form for <code>target_repo</code> and other fields that should not become prose.</li><li>Scalar placeholders: strings may include <code>{context.foo}</code> or <code>{event.foo}</code> placeholders. Each placeholder must resolve to a scalar. Lists and objects are rejected rather than stringified, which prevents accidental JSON blobs or untrusted text from being embedded into task titles.</li></ul>
|
||||
<p>Unsafe action cases are rejected:</p>
|
||||
<ul><li>Any action path outside <code>context.*</code> or <code>event.*</code>.</li><li>Any path containing calls, indexing, arithmetic, filters, or boolean logic.</li><li>Placeholder values that resolve to lists or objects.</li><li><code>for_each</code> values that are not a whole-field <code>context.*</code> or <code>event.*</code> path to a list.</li><li><code>bind_as</code> names that are not simple identifiers.</li></ul>
|
||||
<p>Per-item rule expansion is explicit:</p>
|
||||
<pre>for_each: context.repos.repos
|
||||
bind_as: repo
|
||||
condition: 'context.repo.sbom_age_days > 30'
|
||||
action:
|
||||
task_template: Run SBOM rescan for {context.repo.repo_slug}
|
||||
target_repo: context.repo.repo_slug
|
||||
priority: medium
|
||||
labels: ["sbom", "security", "automated"]</pre>
|
||||
<p>The weekly SBOM staleness definition is the canonical pattern. The State Hub bulk resolver exposes all repository entries at <code>context.repos.repos</code>, the rule binds each item as <code>context.repo</code>, and the strict staleness definition is <code>context.repo.sbom_age_days > 30</code>. Thirty days exactly is not stale; thirty-one days is stale.</p>
|
||||
<h3>Evaluation semantics</h3>
|
||||
<ul><li>All rules in an ActivityDefinition are evaluated; <strong>all matching rules fire</strong> (not first-match-only). There is no implicit ordering beyond the file order, which is documented in the ActivityDefinition for human clarity.</li><li>A rule whose condition raises an error during evaluation is skipped and logged as <code>rule_error</code>; other rules still fire. This prevents a single malformed rule from silencing an entire ActivityDefinition.</li><li>An empty condition (omitted <code>condition</code> field) evaluates to <code>true</code> — the rule always fires when the trigger fires.</li></ul>
|
||||
<h3>Instructions</h3>
|
||||
<p>An Instruction defers the task-creation decision to an LLM. It specifies what context to provide, how to frame the prompt, and what output schema to enforce.</p>
|
||||
<h3>Structure</h3>
|
||||
<pre># in an instruction fenced block:
|
||||
id: {slug}
|
||||
condition: '{expression}' # optional pre-filter (Rule DSL); runs before LLM
|
||||
trusted_fields: # REQUIRED — explicit allowlist of payload fields
|
||||
- event.attributes.repo_slug # safe to interpolate into prompt
|
||||
- event.attributes.domain
|
||||
- event.attributes.tags
|
||||
model: claude-sonnet-4-6
|
||||
review_advisory: false # true | false — advisory evidence, not a gate
|
||||
prompt: |
|
||||
{prompt template — only trusted_fields may be interpolated}
|
||||
output_schema: {path to JSON schema file}</pre>
|
||||
<h3>Trusted fields and prompt injection protection</h3>
|
||||
<p>The <code>trusted_fields</code> list is <strong>required</strong> and enforced at parse time. Any field not listed is unavailable to the prompt template. The template engine raises <code>UntrustedFieldError</code> if the prompt references a field not in <code>trusted_fields</code>.</p>
|
||||
<p>The rationale: event payloads may contain free-text from untrusted sources — commit messages, issue titles, CVE descriptions, repo descriptions. Interpolating these directly into a prompt creates a prompt injection surface. Trusted fields are those whose values are validated by the event type schema (typed attributes like slugs, domain names, tag lists) and cannot carry arbitrary instruction text by construction.</p>
|
||||
<p>Fields of type <code>object</code> (freeform JSON) are <strong>never eligible</strong> for <code>trusted_fields</code> even if listed — the evaluator rejects this at parse time.</p>
|
||||
<h3>Output schema enforcement</h3>
|
||||
<p>The LLM response is validated against <code>output_schema</code> using JSON Schema validation. If validation fails, the instruction retries once with the schema error appended to the prompt. If the second attempt also fails, the instruction records an <code>instruction_output_error</code> audit event and emits no tasks. Tasks are <strong>never created from unvalidated output</strong>.</p>
|
||||
<p>Structured output mode (tool_use / JSON mode) is used where the model supports it. The output schema must define <code>List[TaskSpec]</code> or a compatible envelope.</p>
|
||||
<h3><code>review_advisory: true</code></h3>
|
||||
<p>ACTIVITY-WP-0035 selected advisory-only semantics because no downstream owner currently exposes an acknowledged proposal/decision/release contract. The instruction's task/report evidence is marked <code>review_advisory=true</code> and <code>review_gate_applied=false</code> so an operator can distinguish review-worthy output without interpreting it as held for approval.</p>
|
||||
<p>The legacy input name <code>review_required</code> is accepted during migration and normalized to <code>review_advisory</code>; new definitions must not use it. activity-core does <strong>not</strong> route proposals to a pending-review queue and does not own review lifecycle state. A future hold/release design requires a named downstream owner, an idempotent release reference, an ADR update, and fail-closed emission tests.</p>
|
||||
<h3>Evaluation semantics</h3>
|
||||
<ul><li>Instructions are evaluated <strong>after</strong> all rules in the ActivityDefinition.</li><li>The optional <code>condition</code> field on an instruction uses the same Rule DSL as a first-pass filter — if the condition is false, the LLM is not called. This avoids LLM cost for events that clearly do not need instruction judgement.</li><li>Instructions are <strong>not</strong> first-match-only; all instructions whose conditions pass fire. An ActivityDefinition may have zero instructions.</li></ul>
|
||||
<h3>Audit trail</h3>
|
||||
<p>Every task emission records:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Field</th><th>Rule</th><th>Instruction</th></tr></thead><tbody><tr><td><code>source_type</code></td><td><code>"rule"</code></td><td><code>"instruction"</code></td></tr><tr><td><code>source_id</code></td><td>rule <code>id</code> from definition</td><td>instruction <code>id</code> from definition</td></tr><tr><td><code>source_version</code></td><td>ActivityDefinition version</td><td>ActivityDefinition version</td></tr><tr><td><code>triggering_event_id</code></td><td>event UUID</td><td>event UUID</td></tr><tr><td><code>condition_matched</code></td><td>expression string</td><td>expression string (pre-filter)</td></tr><tr><td><code>prompt_hash</code></td><td>—</td><td>SHA-256 of rendered prompt</td></tr><tr><td><code>model</code></td><td>—</td><td>model ID used</td></tr><tr><td><code>output_validated</code></td><td>—</td><td><code>true</code> / <code>false</code></td></tr><tr><td><code>review_advisory</code></td><td>—</td><td><code>true</code> / <code>false</code>; no gate applied</td></tr></tbody></table></div>
|
||||
<p>The audit trail is written to the <code>task_spawn_log</code> table in activity-core's database and referenced from the task record in issue-core.</p>
|
||||
<p>The rendered prompt and provider response are deliberately not persisted. The prompt hash proves equality when an authorized operator can reconstruct the same input, but the audit contract does not promise reconstruction after source definitions or upstream event retention have changed. <code>activity_runs</code> retains the definition version and bounded context snapshot; reports may retain allowlisted route/usage metadata. Prompts, messages, tool output, credential fields, and provider blobs are excluded from run, progress, and public API evidence.</p>
|
||||
<h3>Testing strategy</h3>
|
||||
<p><strong>Rules</strong>: every rule can and should be unit-tested with fixture event payloads. A test helper <code>evaluate_rule(condition_str, event_fixture)</code> returns <code>bool</code> and raises on syntax errors. Tests live alongside ActivityDefinition files: <code>activity-definitions/{slug}.test.json</code> — a list of <code>{event, expected_rules_fired}</code> fixtures.</p>
|
||||
<p><strong>Instructions</strong>: instructions cannot be deterministically unit-tested. Instead:</p>
|
||||
<ul><li>Sample evaluations are collected: given a fixture event, record the LLM response.</li><li>Samples are committed to <code>activity-definitions/{slug}.samples/</code> for human review.</li><li>Output schema validation is unit-tested independently of the LLM call.</li><li>Prompt injection resistance is tested by including injection strings in fixture event payloads and asserting they do not appear in the rendered prompt.</li></ul>
|
||||
<h3>rules-core module boundary</h3>
|
||||
<p>The rule evaluator and instruction executor live in <code>src/activity_core/rules/</code>. Within this module:</p>
|
||||
<ul><li><strong>No imports from</strong> <code>temporalio</code>, <code>sqlalchemy</code>, <code>fastapi</code>, or any activity-core application code.</li><li>Public surface: <code>evaluate_condition(expr: str, event: EventEnvelope, context: dict) -> bool</code> and <code>execute_instruction(instr: InstructionDef, event: EventEnvelope, context: dict) -> List[TaskSpec]</code>.</li><li>The module is independently importable and testable without starting the Temporal worker or Postgres.</li></ul>
|
||||
<p>This boundary makes future extraction to <code>rules-core</code> a packaging exercise, not a refactor.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>The <code>ActivityDefinition</code> Pydantic model gains <code>rules: List[RuleDef]</code> and <code>instructions: List[InstructionDef]</code> fields. The current implicit "always create tasks" behaviour is replaced by explicit rule blocks.</li><li>A new <code>RuleEvaluator</code> class (AST walker) is added to <code>src/activity_core/rules/</code>.</li><li>A new <code>InstructionExecutor</code> class handles prompt rendering, LLM call, output validation, and review-required audit metadata. Pending review queue routing remains a future issue-core integration.</li><li>Integration tests for rule evaluation use fixture JSON; no running Temporal required.</li><li>The <code>task_spawn_log</code> table is added to the Postgres schema (new Alembic migration).</li><li>ActivityDefinition files that omit both <code>rules</code> and <code>instructions</code> are valid (they fire with no output) — this supports future placeholder definitions.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<p><strong>OPA / Rego for rule conditions</strong>: powerful, well-established policy language, supports complex logic. Rejected — Rego's learning curve is high for non-specialists; agents rarely produce correct Rego without fine-tuning; it adds a runtime dependency. The simple AST-walker DSL covers the realistic condition complexity for this org.</p>
|
||||
<p><strong>Rules as Python lambdas</strong>: maximum expressiveness. Rejected — arbitrary code execution in a rule condition is a serious security surface, especially in an org-wide event loop. Code deployment required for any rule change; agents cannot write rules without code write access.</p>
|
||||
<p><strong>LLM for all conditions (no Rule/Instruction split)</strong>: simpler model, more flexible. Rejected — non-deterministic for cases that are deterministic; expensive for high-frequency events like cron ticks; impossible to unit-test; audit trail for deterministic rules becomes murky.</p>
|
||||
<p><strong>Instructions only, no Rules</strong>: allows arbitrary LLM judgement for everything. Rejected — LLM cost for every event, latency, and non-determinism are unacceptable for high-frequency maintenance automations. Many cases (SBOM staleness check, tag-based routing) are fully deterministic and should stay that way.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ACT-ADR-001 — Event Bridge Architecture</li><li>ACT-ADR-002 — Definition format (where rule/instruction blocks live)</li><li>CUST-TFE-SCOPE-2026-000001 — task-flow-engine extraction (analogue pattern)</li><li><code>src/activity_core/rules/</code> — implementation home</li></ul>
|
||||
</section><footer><span>ACT-ADR-003 · accepted-2 · accepted</span><span>activity-core · docs/adr/adr-003-rule-instruction-model.md · b72fdb5452bff51a867a0316edb994723b35f268</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="5cb88edf4d52a65ea31b1f2f53bcf6f71769d234">
|
||||
<meta name="policy-source-revision" content="6515ed9ef8499cb3de3397f0ae3993cc71440599">
|
||||
<meta name="policy-source-digest" content="a28668fb4b8b6c5ec8c94baac000061276d85ef1849ec7ab8d132b913dbfe3be">
|
||||
<title>Policy addressing and permanence</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>policy-nexus-adr-0001</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>Policy addressing and permanence</h1><p class="sub">Source: <code>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 5cb88edf4d52a65ea31b1f2f53bcf6f71769d234</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#permanence-promise"><span class="n">·</span>Permanence promise</a></li><li><a href="#publication-scope"><span class="n">·</span>Publication scope</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><ul><li>Status: accepted</li><li>Date: 2026-08-18</li><li>Owner: the-custodian</li></ul>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>policy-nexus-adr-0001</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>Policy addressing and permanence</h1><p class="sub">Source: <code>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 6515ed9ef8499cb3de3397f0ae3993cc71440599</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#permanence-promise"><span class="n">·</span>Permanence promise</a></li><li><a href="#publication-scope"><span class="n">·</span>Publication scope</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><ul><li>Status: accepted</li><li>Date: 2026-08-18</li><li>Owner: the-custodian</li></ul>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>A document has one stable current address and immutable revision addresses:</p>
|
||||
<pre>/<kind>/<document>/<version>/
|
||||
|
|
@ -211,4 +211,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Builds fail if a source disappears, an id differs, a path collides, or an immutable revision would change; stale output is not silently called fresh.</li><li>Pages show status, revision, owner, last review and exact source revision.</li><li>Availability remains restart recovery on the single-node rail. This contract promises stable addressing, not a high-availability SLA.</li></ul>
|
||||
</section><footer><span>policy-nexus-adr-0001 · accepted-1 · accepted</span><span>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 5cb88edf4d52a65ea31b1f2f53bcf6f71769d234</span></footer></main></div></div></html>
|
||||
</section><footer><span>policy-nexus-adr-0001 · accepted-1 · accepted</span><span>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 6515ed9ef8499cb3de3397f0ae3993cc71440599</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="7a24e9107fd62772a82d8b2a69692a09ce88eea9+working-tree.a28668fb4b8b">
|
||||
<meta name="policy-source-revision" content="6515ed9ef8499cb3de3397f0ae3993cc71440599">
|
||||
<meta name="policy-source-digest" content="a28668fb4b8b6c5ec8c94baac000061276d85ef1849ec7ab8d132b913dbfe3be">
|
||||
<title>Policy addressing and permanence</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>policy-nexus-adr-0001</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>Policy addressing and permanence</h1><p class="sub">Source: <code>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 7a24e9107fd62772a82d8b2a69692a09ce88eea9+working-tree.a28668fb4b8b</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#permanence-promise"><span class="n">·</span>Permanence promise</a></li><li><a href="#publication-scope"><span class="n">·</span>Publication scope</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><ul><li>Status: accepted</li><li>Date: 2026-08-18</li><li>Owner: the-custodian</li></ul>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>policy-nexus-adr-0001</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>Policy addressing and permanence</h1><p class="sub">Source: <code>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 6515ed9ef8499cb3de3397f0ae3993cc71440599</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#permanence-promise"><span class="n">·</span>Permanence promise</a></li><li><a href="#publication-scope"><span class="n">·</span>Publication scope</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><ul><li>Status: accepted</li><li>Date: 2026-08-18</li><li>Owner: the-custodian</li></ul>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>A document has one stable current address and immutable revision addresses:</p>
|
||||
<pre>/<kind>/<document>/<version>/
|
||||
|
|
@ -211,4 +211,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Builds fail if a source disappears, an id differs, a path collides, or an immutable revision would change; stale output is not silently called fresh.</li><li>Pages show status, revision, owner, last review and exact source revision.</li><li>Availability remains restart recovery on the single-node rail. This contract promises stable addressing, not a high-availability SLA.</li></ul>
|
||||
</section><footer><span>policy-nexus-adr-0001 · accepted-1 · accepted</span><span>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 7a24e9107fd62772a82d8b2a69692a09ce88eea9+working-tree.a28668fb4b8b</span></footer></main></div></div></html>
|
||||
</section><footer><span>policy-nexus-adr-0001 · accepted-1 · accepted</span><span>policy-nexus · docs/adr/ADR-0001-addressing-and-permanence.md · 6515ed9ef8499cb3de3397f0ae3993cc71440599</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="6aef66cd5cf71a48f5b4e14401dc19a755e2b182445b272d5952df0eb0dea8ec">
|
||||
<title>Custodian Agent Runtime — v0.1 Bootstrap Design</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-002</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-03-12</span><span>generated from canonical source — do not edit</span></div><h1>Custodian Agent Runtime — v0.1 Bootstrap Design</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-002-custodian-agent-runtime-design.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2026-09-12</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decisions"><span class="n">·</span>Decisions</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#deferred"><span class="n">·</span>Deferred</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-002</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-03-12</span><span>generated from canonical source — do not edit</span></div><h1>Custodian Agent Runtime — v0.1 Bootstrap Design</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-002-custodian-agent-runtime-design.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2026-09-12</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decisions"><span class="n">·</span>Decisions</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#deferred"><span class="n">·</span>Deferred</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -239,4 +239,4 @@ Act — Execute only sanctioned write operations from the plan</pre>
|
|||
</section>
|
||||
<section id="deferred"><h2>Deferred</h2>
|
||||
<ul><li>Async event loop / daemon mode (Phase 2)</li><li>RAG over canon (Phase 1 roadmap item)</li><li>Tool adapters beyond state-hub HTTP (planned in <code>runtime/tool_adapters/</code>)</li><li>Deployment on Railiance k3s as a scheduled CronJob</li></ul>
|
||||
</section><footer><span>CUST-ADR-002 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-002-custodian-agent-runtime-design.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section><footer><span>CUST-ADR-002 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-002-custodian-agent-runtime-design.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="a454df0e1d227f99ebb36c4abd45c76cc12579086d34f0c0ccfccfd7f4790823">
|
||||
<title>Canon Federation and Concept Ownership Across InfoTech and Commerce</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-006</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>Canon Federation and Concept Ownership Across InfoTech and Commerce</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-006-canon-federation-concept-ownership.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#resolutions"><span class="n">·</span>Resolutions</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-006</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>Canon Federation and Concept Ownership Across InfoTech and Commerce</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-006-canon-federation-concept-ownership.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#resolutions"><span class="n">·</span>Resolutions</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-17. All seven ownership questions are resolved (see Resolutions); content may now move under <code>CFED-WP-0001</code>.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -250,4 +250,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li>ADR-001 — workplans originate as repo files; hub is a read model</li><li>ADR-005 — cross-repo workplans live in dedicated project repos</li><li><code>info-tech-canon/infospace/models/organization/InfoTechCanonOrganizationModel.md:55</code></li><li><code>info-tech-canon/infospace/models/access-control/InfoTechCanonAccessControlModel.md:106</code>, <code>:214</code></li><li><code>info-tech-canon/infospace/models/governance/InfoTechCanonGovernanceModel.md:107</code></li><li><code>info-tech-canon/demand/CapabilityProvisionEconomics.md</code></li><li><code>identity-canon/canon/CanonicalGlossary.md</code>, <code>canon/DesignPrinciples.md</code></li></ul>
|
||||
</section><footer><span>CUST-ADR-006 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-006-canon-federation-concept-ownership.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section><footer><span>CUST-ADR-006 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-006-canon-federation-concept-ownership.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="3b68adfa6ab329e73f857cf691dc405136d2d66a0135e2c37c236aabe4659557">
|
||||
<title>Connectivity-First Network Posture for Custodian Infrastructure</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-004</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-03-26</span><span>generated from canonical source — do not edit</span></div><h1>Connectivity-First Network Posture for Custodian Infrastructure</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-004-connectivity-first-network-posture.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2026-09-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#rationale"><span class="n">·</span>Rationale</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-rejected"><span class="n">·</span>Alternatives Rejected</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-004</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-03-26</span><span>generated from canonical source — do not edit</span></div><h1>Connectivity-First Network Posture for Custodian Infrastructure</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-004-connectivity-first-network-posture.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2026-09-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#rationale"><span class="n">·</span>Rationale</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-rejected"><span class="n">·</span>Alternatives Rejected</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -233,4 +233,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<p>Considered briefly. VPN would solve the connectivity problem but introduces a persistent network layer that all traffic traverses, reducing the explicitness of individual access paths. ops-bridge tunnels are per-service and per-actor, which gives better observability and blast-radius control. VPN is not ruled out as a future complement but is not the primary approach.</p>
|
||||
<h3>Ad-hoc SSH (no ops-bridge)</h3>
|
||||
<p>The pre-ops-bridge approach. Rejected because it has no health checks, no actor attribution, no audit log, and requires manual intervention to restore. ops-bridge formalises the same SSH tunnel pattern with operational discipline.</p>
|
||||
</section><footer><span>CUST-ADR-004 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-004-connectivity-first-network-posture.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section><footer><span>CUST-ADR-004 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-004-connectivity-first-network-posture.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="13195a721d0e579715f5f39ca6f72b5e49c583611e6ca089e6d4618708ae917f">
|
||||
<title>Cross-Repo Workplans Live in Dedicated Project Repos</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-005</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-06-22</span><span>generated from canonical source — do not edit</span></div><h1>Cross-Repo Workplans Live in Dedicated Project Repos</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-005-cross-repo-workplans-project-repos.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2026-12-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#lifecycle"><span class="n">·</span>Lifecycle</a></li><li><a href="#naming"><span class="n">·</span>Naming</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-005</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-06-22</span><span>generated from canonical source — do not edit</span></div><h1>Cross-Repo Workplans Live in Dedicated Project Repos</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-005-cross-repo-workplans-project-repos.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2026-12-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#lifecycle"><span class="n">·</span>Lifecycle</a></li><li><a href="#naming"><span class="n">·</span>Naming</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -222,4 +222,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ADR-001 — Workplans and Work Items Are Repository Artefacts</li><li><code>CUST-WP-0050</code> — Repo Classification & State Hub Registration Redesign (D1)</li><li><code>canon/standards/repo-classification-standard_v1.0.md</code></li></ul>
|
||||
</section><footer><span>CUST-ADR-005 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-005-cross-repo-workplans-project-repos.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section><footer><span>CUST-ADR-005 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-005-cross-repo-workplans-project-repos.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="f94f429c72f6cfd01ee83f1e5689d2d10ae52d7588d7cbd3ca40aca7eef46fb0">
|
||||
<title>Federated Namespaces</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-011</span> <span class="stat">proposed · draft-2</span> <span>the-custodian</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>Federated Namespaces</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-011-federated-namespaces-and-reconciliation-limits.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#reconciliation-tiers-and-where-automation-stops"><span class="n">·</span>Reconciliation tiers, and where automation stops</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#open-question"><span class="n">·</span>Open question</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-011</span> <span class="stat">proposed · draft-2</span> <span>the-custodian</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>Federated Namespaces</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-011-federated-namespaces-and-reconciliation-limits.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#reconciliation-tiers-and-where-automation-stops"><span class="n">·</span>Reconciliation tiers, and where automation stops</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#open-question"><span class="n">·</span>Open question</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Proposed, draft-2. Amends <code>ADR-007</code> decisions 1 and 2; extends <code>ADR-010</code> decision 4; adopts the plane/ladder/posture form and the accuracy-not-altitude conformance rule from <code>ADR-008</code> (Multi-Tenancy Framework).</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -281,4 +281,4 @@ any participant below R2 -> T1 at best; manual thereafter
|
|||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li><code>canon/standards/federated-organization-standard_v1.0.md</code> — bounded autonomy, escalation, sovereignty by default, rebuildability</li><li>ADR-001 — workplans originate as repo files</li><li>ADR-007 — identifier uniqueness and derived identifiers (amended here)</li><li>ADR-008 — Multi-Tenancy Framework; source of the plane/ladder/posture form and the accuracy-not-altitude conformance rule</li><li>ADR-010 — hub authority, local cache, and the two kinds of hub data</li><li><code>CUST-WP-0058</code> — instance-per-client tenancy</li><li><code>SHR-INV-0001</code> — 425-item disposition inventory, T3 cost evidence</li><li><code>RMGR-WP-0004-T02</code> — <code>rmgr conform</code>, the guard machinery</li></ul>
|
||||
</section><footer><span>CUST-ADR-011 · draft-2 · proposed</span><span>the-custodian · canon/architecture/adr-011-federated-namespaces-and-reconciliation-limits.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section><footer><span>CUST-ADR-011 · draft-2 · proposed</span><span>the-custodian · canon/architecture/adr-011-federated-namespaces-and-reconciliation-limits.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-digest" content="8ea4448b7729035ae6bee044867ac9fd8b4940ea7013eb2b0256407cba1a0500">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="5979da20799118259fc19246f51e8cc8f2c0c1be3d414318bd51d5a27d9ad565">
|
||||
<title>Hub Authority, Local Cache, and the Two Kinds of Hub Data</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,13 +191,14 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-010</span> <span class="stat">proposed · draft-1</span> <span>the-custodian</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>Hub Authority, Local Cache, and the Two Kinds of Hub Data</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-010-hub-authority-and-local-cache-model.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#orphan-disposition"><span class="n">·</span>Orphan disposition</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#implementation"><span class="n">·</span>Implementation</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Proposed.</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-010</span> <span class="stat">proposed · draft-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Hub Authority, Local Cache, and the Two Kinds of Hub Data</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-010-hub-authority-and-local-cache-model.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#orphan-disposition"><span class="n">·</span>Orphan disposition</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#implementation"><span class="n">·</span>Implementation</a></li><li><a href="#references"><span class="n">·</span>References</a></li><li><a href="#outcome-2026-08-24"><span class="n">·</span>Outcome (2026-08-24)</a></li><li><a href="#outcome-2026-08-28"><span class="n">·</span>Outcome (2026-08-28)</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Proposed, and <strong>partially superseded by <code>ADR-012</code></strong> (accepted 2026-08-25). Decisions 1, 5 and 6 are sharpened or given a mechanism there; see the notes on each below. Everything else in this ADR remains in force.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Investigating a workplan-identifier flip-flop (<code>ADR-007</code>) revealed that two State Hub instances were operating as <strong>peer databases</strong>, not as a primary and a replica:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Address</th><th>Instance</th><th>Workplans</th></tr></thead><tbody><tr><td><code>127.0.0.1:8000</code> (IPv4)</td><td>local uvicorn on docker postgres</td><td>955</td></tr><tr><td><code>[::1]:8000</code> (IPv6)</td><td>ssh tunnel <code>state-hub-primary</code> → railiance01</td><td>649</td></tr></tbody></table></div>
|
||||
<p>Both listen on port 8000, separated only by IP family, so every tool defaulting to <code>127.0.0.1</code> reached the local instance while believing it was the primary.</p>
|
||||
<div class="rule-quote"><p><strong>Mechanism identified 2026-08-24 by <code>CUST-WP-0067</code>.</strong> This ADR recorded the shared port as the hazard but not why it was <em>silent</em>, which is the part that let it persist. An <code>ssh -L</code> forward with no bind address listens on every loopback family, and <code>ExitOnForwardFailure=yes</code> fires only when <strong>every</strong> requested bind fails. The IPv4 bind lost to the local uvicorn, the IPv6 bind succeeded, and the tunnel therefore reported success while serving an address nothing resolved to by default. The collision was declared in <code>~/.config/bridge/tunnels.yaml</code>, and <code>ops-bridge</code> guarded only tunnel against tunnel, so a non-bridge listener was invisible to it. Fixed by pinning local forwards to <code>127.0.0.1</code>, so a contested port fails loudly (<code>ops-bridge</code> commit <code>2213847</code>). A shared port is recoverable; a shared port that reports healthy is not.</p></div>
|
||||
<p>Measured divergence — 320 records exist locally but not on the primary:</p>
|
||||
<pre>backed by a file that exists on disk 288 fully rebuildable
|
||||
no backing file recorded 28 not rebuildable as-is
|
||||
|
|
@ -207,14 +208,18 @@ same filename, different UUID 4 duplicate registration</pre>
|
|||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>1. One authoritative hub, deriving from repository files.</strong> The central State Hub — running on railiance — is authoritative. It is authoritative <em>as a reading of the repositories</em>, not as a second place data lives. Repository files remain the source of truth (<code>ADR-001</code>).</p>
|
||||
<div class="rule-quote"><p><strong>Sharpened 2026-08-25 by <code>ADR-012</code> decision 1.</strong> "A reading of the repositories" never said <em>which</em> copy of them, and the honest answer was neither the forge nor any particular working copy: the projection derived from whichever checkout last ran the sync. The hub holds no repository files at all and never reads one. <code>ADR-012</code> names the forge as the projection source.</p></div>
|
||||
<p><strong>2. A local hub is a cache, never a database.</strong> Local instances hold a rebuildable projection. A cache may be discarded and reconstructed from the repositories at any time, and losing it must never lose work.</p>
|
||||
<p>This replaces the peer-database arrangement. It is also why the divergence is tractable: a divergent database is a merge problem, a stale cache is a refresh problem.</p>
|
||||
<div class="rule-quote"><p><strong>Corrected 2026-08-24 by <code>CUST-WP-0067</code>.</strong> Those two shapes are not exhaustive, and repository records took a third one. Measured that day: 122 repositories on the cache, 78 on central, <strong>zero central-only</strong>. A strict subset in the cache's favour is neither a merge problem nor a refresh problem — refreshing the cache would have <em>destroyed</em> the 44 extra records rather than reconciling them, and central held no path to re-derive repositories it had never been told existed. The third shape: <strong>cache-only records whose authoritative source exists and is reachable, but was never introduced to central.</strong> Its remedy is re-derivation from source — not refresh, not merge. Of the 44, 43 had a working copy, all 43 were pushed, and 34 carried a classification file; nothing was unrecoverable, but nothing would have recovered itself either. Onboarding by date showed a clean break: central's repository registrations stopped at 2026-07-08 while the workstation kept accepting them.</p></div>
|
||||
<p><strong>3. Local work requires no hub at all.</strong> Repository files are self-describing — identifier, status, tasks, all in frontmatter. Working in a repository requires reading files, not querying an index. A cache is an optimization for cross-repo questions, never a prerequisite for doing work.</p>
|
||||
<p><strong>4. Hub data is classified by origin, and the two kinds have opposite rules.</strong></p>
|
||||
<div class="scroll"><table><thead><tr><th></th><th>File-derived</th><th>Hub-native</th></tr></thead><tbody><tr><td>Examples</td><td>workplans, tasks, statuses, dependencies</td><td>progress events, decisions, inbox messages, token events</td></tr><tr><td>Source of truth</td><td>the repository file</td><td>the hub</td></tr><tr><td>Offline behaviour</td><td>write the file and commit — the commit <em>is</em> the write</td><td>buffer locally, replay when reachable</td></tr><tr><td>Conflict model</td><td><strong>none</strong>; conflicts are git conflicts, resolved in git</td><td><strong>none</strong>; append-only merges regardless of order</td></tr><tr><td>Central accepts pushes?</td><td>no — it derives</td><td>yes</td></tr></tbody></table></div>
|
||||
<p>Neither kind needs a hub-side conflict model. That is the point of the split: if central derives file-backed state, it cannot hold a conflicting version of it — it re-derives whatever git settles on. Two people editing one workplan is a git conflict and belongs to git.</p>
|
||||
<p><strong>5. Central derives file-backed state; it does not accept pushes of it.</strong> "Authoritative" means authoritative <em>reading</em>, so nothing may inject derived state directly. Hub-native records are the exception and keep a real write path.</p>
|
||||
<div class="rule-quote"><p><strong>Sharpened 2026-08-25 by <code>ADR-012</code> decision 6.</strong> This was policy, not practice: nothing derived, and the workstation pushed everything. <code>ADR-012</code> retires push-based sync as the primary path so that "central derives" becomes true rather than aspirational.</p></div>
|
||||
<p><strong>6. Preliminary until confirmed.</strong> Locally registered data and uncommitted repository state are preliminary until the central service has seen them. Mitigation is by changing the repository files and the local cache — never by editing central to match a local view.</p>
|
||||
<div class="rule-quote"><p><strong>Given a mechanism 2026-08-25 by <code>ADR-012</code> decisions 3 and 4.</strong> "Preliminary" was named here but never built, so in practice locally registered data was indistinguishable from derived state once it arrived. It is now a labelled overlay within the same projection — explicitly not a second store — and it retires when the commit carrying it reaches the forge. The prohibition on editing central to match a local view is unchanged.</p></div>
|
||||
<p>Combined with <code>ADR-007</code> decision 2 (identifiers derived from <code>PREFIX-WP-NNNN</code>), "preliminary" largely stops mattering: a cache computes the same identifier central will, so offline-registered data is already correct on arrival and needs confirmation rather than reconciliation.</p>
|
||||
<p><strong>7. Every record has exactly one authoritative hub.</strong> The State Hub retirement splits one hub into several. Multiple central hubs are permitted only under this rule: the owning hub is determined by the record's repository and domain. Without it, the same peer-database divergence recurs at larger scale.</p>
|
||||
<p><strong>8. Cache reads are advisory and must carry their age.</strong> Cross-repo answers from a cache are advisory and should be presented with staleness. For the repository an agent is working in, the file is truth and the cache is never consulted for correctness.</p>
|
||||
|
|
@ -235,4 +240,14 @@ same filename, different UUID 4 duplicate registration</pre>
|
|||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li>ADR-001 — workplans originate as repo files; hub is a read model</li><li>ADR-007 — identifier uniqueness, derived identifiers, worker topology</li><li>Decision <code>747011c6</code> — repository standards belong to Repo Manager</li><li><code>RMGR-WP-0005</code> — registrar consolidation and deterministic identifiers</li><li><code>STATE-WP-0068</code> — offline write buffer and edge relay (rescope candidate)</li><li>Divergence measurement, 2026-08-17: 955 local / 649 primary / 320 local-only</li></ul>
|
||||
</section><footer><span>CUST-ADR-010 · draft-1 · proposed</span><span>the-custodian · canon/architecture/adr-010-hub-authority-and-local-cache-model.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section>
|
||||
<section id="outcome-2026-08-24"><h2>Outcome (2026-08-24)</h2>
|
||||
<div class="rule-quote"><p>Added by <code>CUST-WP-0067</code>. This ADR read as though its remediation had landed. It had not: the two-instance condition it measured on 2026-08-17 was still live seven weeks later, and had continued to accumulate records the whole time. An ADR that describes a fix in the present tense is easily mistaken for a fix that happened — worth stating plainly for the ones that follow.</p></div>
|
||||
<p>The peer-database arrangement is now resolved, by deletion rather than by reconciliation:</p>
|
||||
<ul><li><strong>The local hub instance is retired.</strong> Rather than making two instances coexist safely, the second one was removed. Exactly one process binds <code>127.0.0.1:8000</code> and it is the tunnel to central. Nothing remains that could impersonate the primary, and no call site needed editing — retiring the impersonator made the existing <code>127.0.0.1:8000</code> defaults correct.</li><li><strong>Decision 3 carried the argument.</strong> Because local work requires no hub at all, and Repo Manager already maintains a file-derived index, the local instance was redundant rather than load-bearing.</li><li><strong>Repo Manager gained the write path it was already assigned.</strong> <code>hub-record-authority.yaml</code> gives it <code>managed_repos</code> as <code>file-derived</code>, but it exposed no command for it; the only working path lived in the State Hub repo and defaulted to a local address. <code>rmgr repo-onboard</code> closes that, and refuses to onboard a repository whose backing file is uncommitted, unpushed, or has no upstream — a record whose source is only local cannot be re-derived, which is the failure this ADR exists to prevent.</li><li><strong>33 of the 44 were re-derived onto central</strong>, taking it from 78 to 111 repositories. The remaining 11 carry written dispositions rather than guessed values, per the orphan-disposition principle above.</li></ul>
|
||||
<p>One cause sat deeper than the topology: the hub resolved its classification allowed-values file from three hardcoded developer-workstation checkouts, so in a container every classification write failed. Repository classification could <em>only</em> be written from a workstation. That is a second instance of this ADR's own theme — authority that depends on where a process happens to run is not authority — and is why "central derives" had never been achievable for this record type.</p>
|
||||
</section>
|
||||
<section id="outcome-2026-08-28"><h2>Outcome (2026-08-28)</h2>
|
||||
<div class="rule-quote"><p>Added by <code>CUST-WP-0068</code>. The 2026-08-24 outcome closed the <em>repository</em> divergence. The work-record divergence this ADR originally measured — 955 local / 649 primary — remained, because the retired instance's database was still load-bearing. That is now closed.</p></div>
|
||||
<ul><li><strong>Central holds 1167 workplans.</strong> Records that existed only in the cache were re-derived from their files, renamed onto the canonical scheme, or given a written disposition (<code>docs/recovery/cache-only-disposition-2026-08-28.md</code>).</li><li><strong>No open work record exists only in the cache.</strong> Remaining cache-only slugs are aliases of recovered records, clay-borg product files (not workplans), or prefix-migration residue.</li><li><strong>The cache database is discarded.</strong> Final dump <code>~/backups/state-hub-cache-2026-08-28.dump</code>. Container <code>infra-postgres-1</code> and volume <code>infra_pg_data</code> removed. Port 5432 is free.</li><li><strong>The local instance is no longer load-bearing for any record type.</strong> Decision 3 is now true in operation, not only in argument.</li></ul>
|
||||
</section><footer><span>CUST-ADR-010 · draft-2 · proposed</span><span>the-custodian · canon/architecture/adr-010-hub-authority-and-local-cache-model.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d3c6f13d7aed9784b6f31c23d2b2777668493ba5">
|
||||
<meta name="policy-source-digest" content="5979da20799118259fc19246f51e8cc8f2c0c1be3d414318bd51d5a27d9ad565">
|
||||
<title>Hub Authority, Local Cache, and the Two Kinds of Hub Data</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-010</span> <span class="stat">proposed · draft-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Hub Authority, Local Cache, and the Two Kinds of Hub Data</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-010-hub-authority-and-local-cache-model.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#orphan-disposition"><span class="n">·</span>Orphan disposition</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#implementation"><span class="n">·</span>Implementation</a></li><li><a href="#references"><span class="n">·</span>References</a></li><li><a href="#outcome-2026-08-24"><span class="n">·</span>Outcome (2026-08-24)</a></li><li><a href="#outcome-2026-08-28"><span class="n">·</span>Outcome (2026-08-28)</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Proposed, and <strong>partially superseded by <code>ADR-012</code></strong> (accepted 2026-08-25). Decisions 1, 5 and 6 are sharpened or given a mechanism there; see the notes on each below. Everything else in this ADR remains in force.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Investigating a workplan-identifier flip-flop (<code>ADR-007</code>) revealed that two State Hub instances were operating as <strong>peer databases</strong>, not as a primary and a replica:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Address</th><th>Instance</th><th>Workplans</th></tr></thead><tbody><tr><td><code>127.0.0.1:8000</code> (IPv4)</td><td>local uvicorn on docker postgres</td><td>955</td></tr><tr><td><code>[::1]:8000</code> (IPv6)</td><td>ssh tunnel <code>state-hub-primary</code> → railiance01</td><td>649</td></tr></tbody></table></div>
|
||||
<p>Both listen on port 8000, separated only by IP family, so every tool defaulting to <code>127.0.0.1</code> reached the local instance while believing it was the primary.</p>
|
||||
<div class="rule-quote"><p><strong>Mechanism identified 2026-08-24 by <code>CUST-WP-0067</code>.</strong> This ADR recorded the shared port as the hazard but not why it was <em>silent</em>, which is the part that let it persist. An <code>ssh -L</code> forward with no bind address listens on every loopback family, and <code>ExitOnForwardFailure=yes</code> fires only when <strong>every</strong> requested bind fails. The IPv4 bind lost to the local uvicorn, the IPv6 bind succeeded, and the tunnel therefore reported success while serving an address nothing resolved to by default. The collision was declared in <code>~/.config/bridge/tunnels.yaml</code>, and <code>ops-bridge</code> guarded only tunnel against tunnel, so a non-bridge listener was invisible to it. Fixed by pinning local forwards to <code>127.0.0.1</code>, so a contested port fails loudly (<code>ops-bridge</code> commit <code>2213847</code>). A shared port is recoverable; a shared port that reports healthy is not.</p></div>
|
||||
<p>Measured divergence — 320 records exist locally but not on the primary:</p>
|
||||
<pre>backed by a file that exists on disk 288 fully rebuildable
|
||||
no backing file recorded 28 not rebuildable as-is
|
||||
same filename, different UUID 4 duplicate registration</pre>
|
||||
<p><strong>No file was missing for any of the 288.</strong> The divergence is therefore almost entirely redundant: it can be discarded and rebuilt from repositories without losing anything.</p>
|
||||
<p><code>ADR-001</code> already says work originates as repository files and the hub is a read model. The two-peer-database situation contradicts that in practice: a second database accumulated 306 net records of its own.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>1. One authoritative hub, deriving from repository files.</strong> The central State Hub — running on railiance — is authoritative. It is authoritative <em>as a reading of the repositories</em>, not as a second place data lives. Repository files remain the source of truth (<code>ADR-001</code>).</p>
|
||||
<div class="rule-quote"><p><strong>Sharpened 2026-08-25 by <code>ADR-012</code> decision 1.</strong> "A reading of the repositories" never said <em>which</em> copy of them, and the honest answer was neither the forge nor any particular working copy: the projection derived from whichever checkout last ran the sync. The hub holds no repository files at all and never reads one. <code>ADR-012</code> names the forge as the projection source.</p></div>
|
||||
<p><strong>2. A local hub is a cache, never a database.</strong> Local instances hold a rebuildable projection. A cache may be discarded and reconstructed from the repositories at any time, and losing it must never lose work.</p>
|
||||
<p>This replaces the peer-database arrangement. It is also why the divergence is tractable: a divergent database is a merge problem, a stale cache is a refresh problem.</p>
|
||||
<div class="rule-quote"><p><strong>Corrected 2026-08-24 by <code>CUST-WP-0067</code>.</strong> Those two shapes are not exhaustive, and repository records took a third one. Measured that day: 122 repositories on the cache, 78 on central, <strong>zero central-only</strong>. A strict subset in the cache's favour is neither a merge problem nor a refresh problem — refreshing the cache would have <em>destroyed</em> the 44 extra records rather than reconciling them, and central held no path to re-derive repositories it had never been told existed. The third shape: <strong>cache-only records whose authoritative source exists and is reachable, but was never introduced to central.</strong> Its remedy is re-derivation from source — not refresh, not merge. Of the 44, 43 had a working copy, all 43 were pushed, and 34 carried a classification file; nothing was unrecoverable, but nothing would have recovered itself either. Onboarding by date showed a clean break: central's repository registrations stopped at 2026-07-08 while the workstation kept accepting them.</p></div>
|
||||
<p><strong>3. Local work requires no hub at all.</strong> Repository files are self-describing — identifier, status, tasks, all in frontmatter. Working in a repository requires reading files, not querying an index. A cache is an optimization for cross-repo questions, never a prerequisite for doing work.</p>
|
||||
<p><strong>4. Hub data is classified by origin, and the two kinds have opposite rules.</strong></p>
|
||||
<div class="scroll"><table><thead><tr><th></th><th>File-derived</th><th>Hub-native</th></tr></thead><tbody><tr><td>Examples</td><td>workplans, tasks, statuses, dependencies</td><td>progress events, decisions, inbox messages, token events</td></tr><tr><td>Source of truth</td><td>the repository file</td><td>the hub</td></tr><tr><td>Offline behaviour</td><td>write the file and commit — the commit <em>is</em> the write</td><td>buffer locally, replay when reachable</td></tr><tr><td>Conflict model</td><td><strong>none</strong>; conflicts are git conflicts, resolved in git</td><td><strong>none</strong>; append-only merges regardless of order</td></tr><tr><td>Central accepts pushes?</td><td>no — it derives</td><td>yes</td></tr></tbody></table></div>
|
||||
<p>Neither kind needs a hub-side conflict model. That is the point of the split: if central derives file-backed state, it cannot hold a conflicting version of it — it re-derives whatever git settles on. Two people editing one workplan is a git conflict and belongs to git.</p>
|
||||
<p><strong>5. Central derives file-backed state; it does not accept pushes of it.</strong> "Authoritative" means authoritative <em>reading</em>, so nothing may inject derived state directly. Hub-native records are the exception and keep a real write path.</p>
|
||||
<div class="rule-quote"><p><strong>Sharpened 2026-08-25 by <code>ADR-012</code> decision 6.</strong> This was policy, not practice: nothing derived, and the workstation pushed everything. <code>ADR-012</code> retires push-based sync as the primary path so that "central derives" becomes true rather than aspirational.</p></div>
|
||||
<p><strong>6. Preliminary until confirmed.</strong> Locally registered data and uncommitted repository state are preliminary until the central service has seen them. Mitigation is by changing the repository files and the local cache — never by editing central to match a local view.</p>
|
||||
<div class="rule-quote"><p><strong>Given a mechanism 2026-08-25 by <code>ADR-012</code> decisions 3 and 4.</strong> "Preliminary" was named here but never built, so in practice locally registered data was indistinguishable from derived state once it arrived. It is now a labelled overlay within the same projection — explicitly not a second store — and it retires when the commit carrying it reaches the forge. The prohibition on editing central to match a local view is unchanged.</p></div>
|
||||
<p>Combined with <code>ADR-007</code> decision 2 (identifiers derived from <code>PREFIX-WP-NNNN</code>), "preliminary" largely stops mattering: a cache computes the same identifier central will, so offline-registered data is already correct on arrival and needs confirmation rather than reconciliation.</p>
|
||||
<p><strong>7. Every record has exactly one authoritative hub.</strong> The State Hub retirement splits one hub into several. Multiple central hubs are permitted only under this rule: the owning hub is determined by the record's repository and domain. Without it, the same peer-database divergence recurs at larger scale.</p>
|
||||
<p><strong>8. Cache reads are advisory and must carry their age.</strong> Cross-repo answers from a cache are advisory and should be presented with staleness. For the repository an agent is working in, the file is truth and the cache is never consulted for correctness.</p>
|
||||
</section>
|
||||
<section id="orphan-disposition"><h2>Orphan disposition</h2>
|
||||
<p>The 28 records with no backing file are the only ones a cache rebuild would drop. They fall into three classes, to be separated before any rebuild:</p>
|
||||
<ol><li><strong>Broken links</strong> — a file exists but <code>backing_filename</code> was never recorded. <code>RMGR-WP-0004</code> is one: the workplan file exists and is committed. These are metadata repairs, not data loss, and are likely the largest class.</li><li><strong>Live hub-first records</strong> — <code>proposed</code>, <code>ready</code>, or <code>backlog</code> with no file, in <code>activity-core</code>, <code>core-hub</code>, <code>hub-core</code>, <code>issue-core</code>, <code>ops-hub</code>, <code>prj-forgejo-org-refactor</code>, <code>railiance-enablement</code>, <code>railiance-infra</code>, <code>reef-railiance</code>. Each needs a repository file written or an explicit drop. These are <code>ADR-001</code> violations and must not be preserved as hub-only records.</li><li><strong>Closed hub-first records</strong> — <code>finished</code> or <code>archived</code> with no file. Retain as historical provenance where cheap; do not reconstruct plans that are done.</li></ol>
|
||||
<p>A cache rebuild enforces <code>ADR-001</code> retroactively: the only casualties are records that broke it.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><strong>Positive.</strong> The divergence becomes discardable rather than mergeable. Offline work is fully supported without a write buffer for file-backed state — the git commit is the write. No hub-side conflict model is needed for either data kind. Authority stops being a policy claim and becomes a structural property.</p>
|
||||
<p><strong>Negative.</strong> The 28 orphans require case-by-case disposition before a rebuild. Any consumer that treats a local hub as authoritative must be corrected. The one-hub-per-record rule constrains the retirement's hub split.</p>
|
||||
<p><strong>Rescoping.</strong> <code>STATE-WP-0068</code> (offline write buffer and edge relay) is scoped as a single mechanism. Under decision 4, most of what it buffers does not need buffering — only the append-only hub-native stream does. Its scope should be re-examined before more is built on it; this may reduce work rather than add it.</p>
|
||||
<p><strong>Correction to <code>ADR-007</code>.</strong> Decision 2 there calls the workstation instance a "development read replica". It was neither a replica nor smaller — it held 306 more workplans than the primary. Superseded by decisions 1–3 here.</p>
|
||||
</section>
|
||||
<section id="implementation"><h2>Implementation</h2>
|
||||
<p>Owned by <code>repo-manager</code> for file-derived state (decision <code>747011c6</code>; it already owns repository representation, file-backed record indexing, and reconciliation) and by <code>hub-core</code> for hub-native records. Tracked under <code>RMGR-WP-0005</code>.</p>
|
||||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li>ADR-001 — workplans originate as repo files; hub is a read model</li><li>ADR-007 — identifier uniqueness, derived identifiers, worker topology</li><li>Decision <code>747011c6</code> — repository standards belong to Repo Manager</li><li><code>RMGR-WP-0005</code> — registrar consolidation and deterministic identifiers</li><li><code>STATE-WP-0068</code> — offline write buffer and edge relay (rescope candidate)</li><li>Divergence measurement, 2026-08-17: 955 local / 649 primary / 320 local-only</li></ul>
|
||||
</section>
|
||||
<section id="outcome-2026-08-24"><h2>Outcome (2026-08-24)</h2>
|
||||
<div class="rule-quote"><p>Added by <code>CUST-WP-0067</code>. This ADR read as though its remediation had landed. It had not: the two-instance condition it measured on 2026-08-17 was still live seven weeks later, and had continued to accumulate records the whole time. An ADR that describes a fix in the present tense is easily mistaken for a fix that happened — worth stating plainly for the ones that follow.</p></div>
|
||||
<p>The peer-database arrangement is now resolved, by deletion rather than by reconciliation:</p>
|
||||
<ul><li><strong>The local hub instance is retired.</strong> Rather than making two instances coexist safely, the second one was removed. Exactly one process binds <code>127.0.0.1:8000</code> and it is the tunnel to central. Nothing remains that could impersonate the primary, and no call site needed editing — retiring the impersonator made the existing <code>127.0.0.1:8000</code> defaults correct.</li><li><strong>Decision 3 carried the argument.</strong> Because local work requires no hub at all, and Repo Manager already maintains a file-derived index, the local instance was redundant rather than load-bearing.</li><li><strong>Repo Manager gained the write path it was already assigned.</strong> <code>hub-record-authority.yaml</code> gives it <code>managed_repos</code> as <code>file-derived</code>, but it exposed no command for it; the only working path lived in the State Hub repo and defaulted to a local address. <code>rmgr repo-onboard</code> closes that, and refuses to onboard a repository whose backing file is uncommitted, unpushed, or has no upstream — a record whose source is only local cannot be re-derived, which is the failure this ADR exists to prevent.</li><li><strong>33 of the 44 were re-derived onto central</strong>, taking it from 78 to 111 repositories. The remaining 11 carry written dispositions rather than guessed values, per the orphan-disposition principle above.</li></ul>
|
||||
<p>One cause sat deeper than the topology: the hub resolved its classification allowed-values file from three hardcoded developer-workstation checkouts, so in a container every classification write failed. Repository classification could <em>only</em> be written from a workstation. That is a second instance of this ADR's own theme — authority that depends on where a process happens to run is not authority — and is why "central derives" had never been achievable for this record type.</p>
|
||||
</section>
|
||||
<section id="outcome-2026-08-28"><h2>Outcome (2026-08-28)</h2>
|
||||
<div class="rule-quote"><p>Added by <code>CUST-WP-0068</code>. The 2026-08-24 outcome closed the <em>repository</em> divergence. The work-record divergence this ADR originally measured — 955 local / 649 primary — remained, because the retired instance's database was still load-bearing. That is now closed.</p></div>
|
||||
<ul><li><strong>Central holds 1167 workplans.</strong> Records that existed only in the cache were re-derived from their files, renamed onto the canonical scheme, or given a written disposition (<code>docs/recovery/cache-only-disposition-2026-08-28.md</code>).</li><li><strong>No open work record exists only in the cache.</strong> Remaining cache-only slugs are aliases of recovered records, clay-borg product files (not workplans), or prefix-migration residue.</li><li><strong>The cache database is discarded.</strong> Final dump <code>~/backups/state-hub-cache-2026-08-28.dump</code>. Container <code>infra-postgres-1</code> and volume <code>infra_pg_data</code> removed. Port 5432 is free.</li><li><strong>The local instance is no longer load-bearing for any record type.</strong> Decision 3 is now true in operation, not only in argument.</li></ul>
|
||||
</section><footer><span>CUST-ADR-010 · draft-2 · proposed</span><span>the-custodian · canon/architecture/adr-010-hub-authority-and-local-cache-model.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-digest" content="350c26d86c573716eb12333473917d91b8cd68b8798197b0077af1a6ba8c6480">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="fcb49719e2b85b9120c0bd5bebd5e82748ca713f16b44951c028b60205514e2b">
|
||||
<title>Materialized Derived State with Fingerprint Invalidation for Repo-Sourced Data</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,8 +191,8 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-003</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-03-20</span><span>generated from canonical source — do not edit</span></div><h1>Materialized Derived State with Fingerprint Invalidation for Repo-Sourced Data</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-003-materialized-derived-state.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2026-09-20</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#pattern-name"><span class="n">·</span>Pattern Name</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#implementation-checklist"><span class="n">·</span>Implementation Checklist</a></li><li><a href="#current-implementations"><span class="n">·</span>Current Implementations</a></li><li><a href="#planned-applications"><span class="n">·</span>Planned Applications</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-003</span> <span class="stat">accepted · accepted-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Materialized Derived State with Fingerprint Invalidation for Repo-Sourced Data</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-003-materialized-derived-state.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#pattern-name"><span class="n">·</span>Pattern Name</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#implementation-checklist"><span class="n">·</span>Implementation Checklist</a></li><li><a href="#current-implementations"><span class="n">·</span>Current Implementations</a></li><li><a href="#planned-applications"><span class="n">·</span>Planned Applications</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted, and <strong>partially superseded by <code>ADR-012</code></strong> (accepted 2026-08-25). Decision 2's fingerprint composition is invalidated in part; decision 5's rebuild principle is given a concrete source and a required operation. See the notes on each.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The Custodian State Hub is a <strong>read model</strong> (CQRS terminology) — its data is fully derivable from canonical sources that live in repositories and the filesystem. No state-hub data is authoritative; it is always a derived view of what the repos contain.</p>
|
||||
|
|
@ -213,14 +213,18 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<h3>2. Each materialised table MUST carry a <code>fingerprint</code> column</h3>
|
||||
<p>The fingerprint is a deterministic string encoding all inputs that affect the computed result. It is compared on each read; if unchanged, the stored result is returned without recomputation. If changed, the result is recomputed and the stored value is updated.</p>
|
||||
<p><strong>Fingerprint composition rules:</strong></p>
|
||||
<ul><li>Include the <code>updated_at</code> timestamp of every DB record that feeds the computation (repo record, related domain, goals, snapshots).</li><li>Include the <code>mtime</code> (filesystem modification time) of every file that feeds the computation (<code>SCOPE.md</code>, <code>CLAUDE.md</code>, lockfiles, <code>tpsc.yaml</code>, etc.).</li><li>Join all components with <code>|</code> as a pipe-separated string — no hashing needed since the string is compared by equality, not transmitted to clients.</li><li>If a file is absent, encode <code>filename:absent</code> rather than omitting it, so file creation also triggers invalidation.</li></ul>
|
||||
<ul><li>Include the <code>updated_at</code> timestamp of every DB record that feeds the computation (repo record, related domain, goals, snapshots).</li><li>Include the <code>mtime</code> (filesystem modification time) of every file that feeds the computation (<code>SCOPE.md</code>, <code>CLAUDE.md</code>, lockfiles, <code>tpsc.yaml</code>, etc.).</li></ul>
|
||||
<div class="rule-quote"><p><strong>Invalidated in part 2026-08-25 by <code>ADR-012</code> decisions 1 and 2.</strong> Filesystem <code>mtime</code> is not a property of the source. It differs between machines, changes on a fresh clone, and says nothing about content — so a fingerprint built from it describes one workstation's filesystem rather than the repository. Under <code>ADR-012</code> the projection derives from the forge, and the commit that produced a record is both the correct input and the auditable one. This was not merely theoretical drift. <code>git_fingerprint</code> for <code>the-custodian</code> held the repository's <em>initial</em> commit while <code>last_state_synced_at</code> was minutes old: the field meant to identify what a projection reflects was wrong by the entire history of the repository, and nothing noticed. Replace <code>mtime</code> inputs with the source commit.</p></div>
|
||||
<ul><li>Join all components with <code>|</code> as a pipe-separated string — no hashing needed since the string is compared by equality, not transmitted to clients.</li><li>If a file is absent, encode <code>filename:absent</code> rather than omitting it, so file creation also triggers invalidation.</li></ul>
|
||||
<p><strong>Reference implementation:</strong> <code>state-hub/api/doi_engine.py::compute_fingerprint()</code></p>
|
||||
<h3>3. Every materialised endpoint MUST support <code>?force_refresh=true</code></h3>
|
||||
<p>Callers must always be able to bypass the cache and trigger a fresh computation. This is the escape hatch for debugging, post-ingest verification, and scheduled background refresh jobs.</p>
|
||||
<h3>4. Writes to source data SHOULD update the repo record's <code>updated_at</code></h3>
|
||||
<p>Operations that change source data (SBOM ingest, TPSC ingest, capability ingest) must ensure <code>managed_repos.updated_at</code> is refreshed so the fingerprint detects the change on the next read. Where data lives in a related table (e.g. <code>tpsc_snapshots</code>), the fingerprint must include that table's <code>max(snapshot_at)</code> directly rather than relying on the repo record.</p>
|
||||
<h3>5. The DB is never the source of truth — the rebuild principle holds</h3>
|
||||
<p>Per ADR-001, the state-hub must be rebuildable from scratch by re-ingesting all canonical sources. Materialised tables are <strong>caches</strong>, not records of authority. They may be wiped and repopulated at any time without data loss. This means:</p>
|
||||
<p>Per ADR-001, the state-hub must be rebuildable from scratch by re-ingesting all canonical sources. Materialised tables are <strong>caches</strong>, not records of authority. They may be wiped and repopulated at any time without data loss.</p>
|
||||
<div class="rule-quote"><p><strong>Given concrete form 2026-08-25 by <code>ADR-012</code> decision 7.</strong> This principle was correct and, until now, never exercised — an untested rebuild path is an assumption rather than a capability, and this one was believed for long enough that a divergence survived seven weeks behind it. <code>ADR-012</code> requires the reconstruction to exist as a routine operation, scoped per repository, sourced from the forge, and verifiable against it. The claim "without data loss" also needs its precondition stated: it holds only while the rule immediately below does. On 2026-08-25, 111 work records existed only in the hub, so a rebuild at that moment would have destroyed them. <code>ADR-012</code> therefore requires reset to refuse, per repository, when records have no counterpart in the forge.</p></div>
|
||||
<p>This means:</p>
|
||||
<ul><li>No materialised table may be the only copy of any information.</li><li>Schema migrations that wipe a materialised table are safe and expected.</li><li>Background jobs that periodically re-ingest all repos are valid and encouraged.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
|
|
@ -241,4 +245,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ADR-001: Workplans and Work Items Are Repository Artefacts</li><li>ADR-002: Custodian Agent Runtime Design</li><li><code>state-hub/api/doi_engine.py</code> — reference implementation</li><li><code>state-hub/api/models/doi_cache.py</code> — reference schema</li><li><code>state-hub/migrations/versions/k8f9a0b1c2d3_doi_cache.py</code> — reference migration</li></ul>
|
||||
</section><footer><span>CUST-ADR-003 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-003-materialized-derived-state.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section><footer><span>CUST-ADR-003 · accepted-2 · accepted</span><span>the-custodian · canon/architecture/adr-003-materialized-derived-state.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d3c6f13d7aed9784b6f31c23d2b2777668493ba5">
|
||||
<meta name="policy-source-digest" content="fcb49719e2b85b9120c0bd5bebd5e82748ca713f16b44951c028b60205514e2b">
|
||||
<title>Materialized Derived State with Fingerprint Invalidation for Repo-Sourced Data</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-003</span> <span class="stat">accepted · accepted-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Materialized Derived State with Fingerprint Invalidation for Repo-Sourced Data</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-003-materialized-derived-state.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#pattern-name"><span class="n">·</span>Pattern Name</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#implementation-checklist"><span class="n">·</span>Implementation Checklist</a></li><li><a href="#current-implementations"><span class="n">·</span>Current Implementations</a></li><li><a href="#planned-applications"><span class="n">·</span>Planned Applications</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted, and <strong>partially superseded by <code>ADR-012</code></strong> (accepted 2026-08-25). Decision 2's fingerprint composition is invalidated in part; decision 5's rebuild principle is given a concrete source and a required operation. See the notes on each.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The Custodian State Hub is a <strong>read model</strong> (CQRS terminology) — its data is fully derivable from canonical sources that live in repositories and the filesystem. No state-hub data is authoritative; it is always a derived view of what the repos contain.</p>
|
||||
<p>Several categories of data fit this description:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Data</th><th>Canonical source</th><th>State-hub table</th></tr></thead><tbody><tr><td>SBOM dependencies</td><td><code>uv.lock</code>, <code>package-lock.json</code>, etc.</td><td><code>sbom_entries</code></td></tr><tr><td>Third-party service declarations</td><td><code>tpsc.yaml</code></td><td><code>tpsc_entries</code></td></tr><tr><td>Provided capabilities</td><td><code>SCOPE.md</code> <code>capability</code> blocks</td><td><code>capability_catalog</code></td></tr><tr><td>DoI compliance tier</td><td>14 criteria across repo files + DB</td><td><code>doi_cache</code></td></tr><tr><td>Workplan task status</td><td><code>workplans/*.md</code></td><td><code>tasks</code></td></tr></tbody></table></div>
|
||||
<p>Early implementations either recomputed this data on every request (too slow) or ingested it once without invalidation (stale data goes undetected). Neither is acceptable for a system designed to give accurate, fast orientation.</p>
|
||||
<p>The <code>doi_cache</code> table, introduced in CUST-WP-0024, demonstrated a pattern that solves both problems. This ADR formalises that pattern and mandates its use for all repo-sourced derived data.</p>
|
||||
</section>
|
||||
<section id="pattern-name"><h2>Pattern Name</h2>
|
||||
<p><strong>Materialized Derived State with Fingerprint Invalidation.</strong></p>
|
||||
<p>This pattern is known under several names in the literature:</p>
|
||||
<ul><li><strong>Materialized View</strong> (SQL standard, PostgreSQL) — the stored result of a query or computation, refreshed on demand when source data changes.</li><li><strong>Derived Data Store</strong> (Kleppmann, <em>Designing Data-Intensive Applications</em>, Ch. 3 & 11) — a system whose entire dataset can be rebuilt from upstream sources; it is never the source of truth.</li><li><strong>Read Model / Projection</strong> (CQRS / Event Sourcing) — a pre-computed view maintained alongside a write model, rebuilt when relevant events occur.</li><li><strong>Fingerprint-based / Content-addressed invalidation</strong> — analogous to HTTP ETags: a cache entry is valid as long as a composite hash/timestamp of its inputs matches the stored value.</li></ul>
|
||||
<p>The State Hub already documents itself as a read model. This ADR extends that principle to specify <em>how</em> the read model stays fresh.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. All repo-sourced derived data MUST be materialised in the DB</h3>
|
||||
<p>Data computed from repository files or repo records must be stored in a dedicated table rather than recomputed per request. Direct computation on every API call is only permissible for development tooling or when explicitly forced by the caller.</p>
|
||||
<h3>2. Each materialised table MUST carry a <code>fingerprint</code> column</h3>
|
||||
<p>The fingerprint is a deterministic string encoding all inputs that affect the computed result. It is compared on each read; if unchanged, the stored result is returned without recomputation. If changed, the result is recomputed and the stored value is updated.</p>
|
||||
<p><strong>Fingerprint composition rules:</strong></p>
|
||||
<ul><li>Include the <code>updated_at</code> timestamp of every DB record that feeds the computation (repo record, related domain, goals, snapshots).</li><li>Include the <code>mtime</code> (filesystem modification time) of every file that feeds the computation (<code>SCOPE.md</code>, <code>CLAUDE.md</code>, lockfiles, <code>tpsc.yaml</code>, etc.).</li></ul>
|
||||
<div class="rule-quote"><p><strong>Invalidated in part 2026-08-25 by <code>ADR-012</code> decisions 1 and 2.</strong> Filesystem <code>mtime</code> is not a property of the source. It differs between machines, changes on a fresh clone, and says nothing about content — so a fingerprint built from it describes one workstation's filesystem rather than the repository. Under <code>ADR-012</code> the projection derives from the forge, and the commit that produced a record is both the correct input and the auditable one. This was not merely theoretical drift. <code>git_fingerprint</code> for <code>the-custodian</code> held the repository's <em>initial</em> commit while <code>last_state_synced_at</code> was minutes old: the field meant to identify what a projection reflects was wrong by the entire history of the repository, and nothing noticed. Replace <code>mtime</code> inputs with the source commit.</p></div>
|
||||
<ul><li>Join all components with <code>|</code> as a pipe-separated string — no hashing needed since the string is compared by equality, not transmitted to clients.</li><li>If a file is absent, encode <code>filename:absent</code> rather than omitting it, so file creation also triggers invalidation.</li></ul>
|
||||
<p><strong>Reference implementation:</strong> <code>state-hub/api/doi_engine.py::compute_fingerprint()</code></p>
|
||||
<h3>3. Every materialised endpoint MUST support <code>?force_refresh=true</code></h3>
|
||||
<p>Callers must always be able to bypass the cache and trigger a fresh computation. This is the escape hatch for debugging, post-ingest verification, and scheduled background refresh jobs.</p>
|
||||
<h3>4. Writes to source data SHOULD update the repo record's <code>updated_at</code></h3>
|
||||
<p>Operations that change source data (SBOM ingest, TPSC ingest, capability ingest) must ensure <code>managed_repos.updated_at</code> is refreshed so the fingerprint detects the change on the next read. Where data lives in a related table (e.g. <code>tpsc_snapshots</code>), the fingerprint must include that table's <code>max(snapshot_at)</code> directly rather than relying on the repo record.</p>
|
||||
<h3>5. The DB is never the source of truth — the rebuild principle holds</h3>
|
||||
<p>Per ADR-001, the state-hub must be rebuildable from scratch by re-ingesting all canonical sources. Materialised tables are <strong>caches</strong>, not records of authority. They may be wiped and repopulated at any time without data loss.</p>
|
||||
<div class="rule-quote"><p><strong>Given concrete form 2026-08-25 by <code>ADR-012</code> decision 7.</strong> This principle was correct and, until now, never exercised — an untested rebuild path is an assumption rather than a capability, and this one was believed for long enough that a divergence survived seven weeks behind it. <code>ADR-012</code> requires the reconstruction to exist as a routine operation, scoped per repository, sourced from the forge, and verifiable against it. The claim "without data loss" also needs its precondition stated: it holds only while the rule immediately below does. On 2026-08-25, 111 work records existed only in the hub, so a rebuild at that moment would have destroyed them. <code>ADR-012</code> therefore requires reset to refuse, per repository, when records have no counterpart in the forge.</p></div>
|
||||
<p>This means:</p>
|
||||
<ul><li>No materialised table may be the only copy of any information.</li><li>Schema migrations that wipe a materialised table are safe and expected.</li><li>Background jobs that periodically re-ingest all repos are valid and encouraged.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<h3>Positive</h3>
|
||||
<ul><li><strong>Fast reads in steady state</strong> — after the first computation, subsequent reads hit the DB with no filesystem or subprocess overhead.</li><li><strong>Accurate on change</strong> — fingerprint invalidation ensures stale data is never silently served; the cache refreshes exactly when needed.</li><li><strong>Debuggable</strong> — <code>force_refresh=true</code> and <code>checked_at</code> timestamps make it easy to see when a value was last computed and to trigger a recheck.</li><li><strong>Consistent with the read model principle</strong> — the pattern makes explicit what was always implied: state-hub data is derived, not authoritative.</li></ul>
|
||||
<h3>Negative / Trade-offs</h3>
|
||||
<ul><li><strong>First-call latency</strong> — cache misses are expensive (filesystem reads, subprocess calls, HTTP self-calls). Mitigated by pre-warming caches at startup or after ingest.</li><li><strong>Fingerprint completeness</strong> — if a new input is added to a computation and not added to the fingerprint, stale results will be silently returned. The fingerprint must be kept in sync with the computation.</li><li><strong>Filesystem dependency</strong> — file mtimes are volatile (e.g. <code>git checkout</code> rewrites mtimes). In practice this means a cache miss after every checkout, not a correctness problem.</li></ul>
|
||||
</section>
|
||||
<section id="implementation-checklist"><h2>Implementation Checklist</h2>
|
||||
<p>When adding a new category of repo-sourced derived data:</p>
|
||||
<ul><li>[ ] Create a <code>_cache</code> or <code>_snapshots</code> table with <code>fingerprint</code> and <code>checked_at</code> columns.</li><li>[ ] Implement <code>compute_fingerprint(repo, ...)</code> in the relevant module.</li><li>[ ] Add <code>?force_refresh=true</code> query parameter to the read endpoint.</li><li>[ ] Ensure the ingest script (or write path) touches <code>managed_repos.updated_at</code> or includes a related table's <code>max(timestamp)</code> in the fingerprint.</li><li>[ ] Verify the cache can be wiped and repopulated without data loss.</li><li>[ ] Document which inputs are included in the fingerprint in a comment alongside <code>compute_fingerprint</code>.</li></ul>
|
||||
</section>
|
||||
<section id="current-implementations"><h2>Current Implementations</h2>
|
||||
<div class="scroll"><table><thead><tr><th>Derived data</th><th>Table</th><th>Fingerprint inputs</th><th>Force-refresh</th></tr></thead><tbody><tr><td>DoI compliance tier</td><td><code>doi_cache</code></td><td><code>repo.updated_at</code>, <code>max(tpsc_snapshots.snapshot_at)</code>, <code>max(repo_goals.updated_at)</code>, <code>mtime(SCOPE.md)</code>, <code>mtime(CLAUDE.md)</code>, <code>mtime(tpsc.yaml)</code></td><td><code>?force_refresh=true</code></td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section id="planned-applications"><h2>Planned Applications</h2>
|
||||
<div class="scroll"><table><thead><tr><th>Derived data</th><th>Table (proposed)</th><th>Notes</th></tr></thead><tbody><tr><td>SBOM summary stats</td><td><code>sbom_cache</code></td><td>Fingerprint: <code>max(sbom_snapshots.snapshot_at)</code></td></tr><tr><td>Capability declarations</td><td><code>capability_cache</code></td><td>Fingerprint: <code>mtime(SCOPE.md)</code>, <code>repo.updated_at</code></td></tr><tr><td>Workplan status summary</td><td>Already handled by consistency checker</td><td>Fingerprint: workplan file mtimes</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ADR-001: Workplans and Work Items Are Repository Artefacts</li><li>ADR-002: Custodian Agent Runtime Design</li><li><code>state-hub/api/doi_engine.py</code> — reference implementation</li><li><code>state-hub/api/models/doi_cache.py</code> — reference schema</li><li><code>state-hub/migrations/versions/k8f9a0b1c2d3_doi_cache.py</code> — reference migration</li></ul>
|
||||
</section><footer><span>CUST-ADR-003 · accepted-2 · accepted</span><span>the-custodian · canon/architecture/adr-003-materialized-derived-state.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</span></footer></main></div></div></html>
|
||||
248
build/adr/custodian-projection-source-overlay/v1/index.html
Normal file
248
build/adr/custodian-projection-source-overlay/v1/index.html
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="b5e8582459f546ae789ad5fd62f458454aa19997b520e32b6b9f792d6af55987">
|
||||
<title>What the Hub Projects</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-012</span> <span class="stat">accepted · 1.0</span> <span>the-custodian</span> <span>reviewed 2026-08-25</span><span>generated from canonical source — do not edit</span></div><h1>What the Hub Projects</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-012-projection-source-and-preliminary-overlay.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2027-02-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#relationship-to-prior-decisions"><span class="n">·</span>Relationship to prior decisions</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted 2026-08-25</strong> by Bernd Worsch. Supersedes <code>ADR-010</code> decision 1's phrase "authoritative as a reading of the repositories" by making the reading concrete, and implements decision 6's unbuilt notion of "preliminary".</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>ADR-001</code> says work originates as repository files and the hub is a read model. <code>ADR-010</code> says central is authoritative <em>as a reading of the repositories</em>. Both leave one question unanswered, and it turns out to be the load-bearing one:</p>
|
||||
<div class="rule-quote"><p><strong>Which</strong> repository files? The forge, or a working copy?</p></div>
|
||||
<p>Verified on 2026-08-25, the answer today is <strong>neither</strong>.</p>
|
||||
<p>Central runs one FastAPI pod on railiance01 with a CNPG database. Inspected from inside, it holds <code>/app</code> — its own source — and nothing else. No clone tree, no repository mount; the <code>sweep</code> hostPath that would give it one is disabled. <strong>The hub never reads a repository.</strong> It cannot see the forge, and it cannot see a workstation.</p>
|
||||
<p>What actually happens is the inverse of the ADR language: <code>fix-consistency</code> and <code>registrar-reconcile</code> run <em>on a workstation</em>, read files in <code>/home/<user>/<repo></code>, and POST the result over a tunnel. The <code>host_paths</code> field records this plainly — 117 repositories carry a path on <code>bnt-lap001</code>, a laptop.</p>
|
||||
<p>So the projection derives from <strong>whichever checkout most recently ran the sync</strong>. That is a third thing, distinct from both the forge and any particular working copy, and it is written down nowhere.</p>
|
||||
<h3>Why this is not academic</h3>
|
||||
<p>Every failure investigated under <code>CUST-WP-0067</code> and <code>CUST-WP-0068</code> is downstream of it:</p>
|
||||
<ul><li>A local hub and central both served port 8000, and every default reached the local one for seven weeks. Neither could be distinguished by what it projected.</li><li>50 repository records pointed at a retired forge, so the sync could not match the checkout and silently registered nothing.</li><li>Duplicate registrations accumulated: the same file registered twice under two identifiers, because two environments each believed they were projecting it.</li><li><code>git_fingerprint</code> — the field that should say which commit a projection reflects — still holds the <em>initial commit</em> for <code>the-custodian</code>, while <code>last_state_synced_at</code> is minutes old. There is no reliable way to ask central what state it is a projection <em>of</em>.</li></ul>
|
||||
<h3>The forcing function</h3>
|
||||
<p>With one contributor, "last writer wins" is invisible, because there is only one writer. With several, each pushes a projection of a different repository state into one shared read model, and each will look locally consistent while disagreeing with the others. That is the peer-database problem <code>ADR-010</code> diagnosed, relocated one layer up and made harder to see.</p>
|
||||
<p>The window to fix this cheaply is before the second contributor, not after.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>1. The forge is the projection source.</strong> Central derives its projection from Forgejo — the pushed state of the default branch — not from any working copy. "Authoritative as a reading of the repositories" now names a specific reading: what the forge holds.</p>
|
||||
<p>Forgejo runs in the same cluster as central, so this is an in-cluster fetch. No tunnel, no cross-network credentials, and central already carries a <code>git</code> binary.</p>
|
||||
<p><strong>2. Every projected record carries the commit it derived from.</strong> A projection without provenance cannot be audited, and the existing <code>git_fingerprint</code> proves the point by being wrong and silent about it. Records must reference the commit that produced them, and a projection whose commit is older than the forge's head must be visibly stale rather than quietly current.</p>
|
||||
<p><strong>3. Unpushed local work is a preliminary overlay, never the baseline.</strong> A working copy may contribute records the forge does not yet hold. They are marked preliminary, attributed to their environment, and never silently merged into the derived baseline. When the commit that carries them reaches the forge, the baseline absorbs them and the overlay entry retires.</p>
|
||||
<p>This implements <code>ADR-010</code> decision 6, which named "preliminary" but left it unbuilt.</p>
|
||||
<p><strong>4. The overlay is a label, not a second store.</strong> Preliminary records live in the same projection, flagged. They are not a parallel database and not a local hub. Every local projection this ecosystem has had was eventually mistaken for authority; the defence is that there is nothing separate to mistake.</p>
|
||||
<p><strong>5. A working copy is a workspace, not a source of authority.</strong> Deleting a checkout after pushing must lose nothing and invalidate nothing. Validation of a projection must not require a local clone of the repository being validated.</p>
|
||||
<p><strong>6. Push-based sync is retired as the primary path.</strong> Workstation tooling stops being how records reach central. It may continue to <em>propose</em> preliminary records under decision 3, but the baseline is derived, not received. This closes the gap where "central derives" was policy while "the laptop pushes" was practice.</p>
|
||||
<p><strong>7. The projection must be resettable from the forge, as a routine operation.</strong> There must be a supported way to reconcile the projection — for one repository or for the whole fleet — against what Forgejo holds: create what is missing, update what differs, and <strong>retire what no longer derives</strong>.</p>
|
||||
<div class="rule-quote"><p><strong>Amended 2026-08-25, before implementation.</strong> This decision originally said <em>discard and rebuild</em>. That operation cannot exist, and the reason is a good one. <code>progress_events</code>, <code>tasks</code>, <code>decisions</code> and <code>review_contracts</code> all reference workplans with <code>ON DELETE RESTRICT</code>, and 1067 of 1075 workplans carry at least one such reference. Deleting a workplan would mean deleting the hub-native records attached to it — which <code>ADR-010</code> decision 4 forbids, and which the schema refuses to allow. The database is enforcing this ADR's own boundary one layer down. A progress event recording work on a workplan is a fact that happened; destroying it to tidy a derived projection would be losing hub-native truth to fix a derived-state problem, which is exactly backwards. Retirement achieves what reset is for — the projection converges on the forge, and records that no longer derive stop appearing as live work — without erasing anything that only the hub holds. Reset reconciles; it does not destroy.</p></div>
|
||||
<p>This is the decision that makes the others checkable rather than merely stated. A read model that cannot be rebuilt from its source is not a projection; it is a database with a projection's reputation, and the difference only becomes visible on the day someone needs to rebuild it. <code>ADR-010</code> decision 2 already asserts that a cache "may be discarded and reconstructed from the repositories at any time" — that claim has never been executed, and an untested rebuild path is an assumption, not a capability.</p>
|
||||
<p>Three properties make it real rather than ceremonial:</p>
|
||||
<ul><li><strong>Routine, not emergency.</strong> It should be run deliberately and often enough that it is known to work, not discovered under pressure. A reset that has never been performed is indistinguishable from one that does not work.</li><li><strong>Per repository is the unit.</strong> Not a convenience over a fleet-wide operation — the repository <em>is</em> the unit of reconstruction, and the fleet-wide form is defined as iteration over it.</li></ul>
|
||||
<p>This follows from the source. Each repository is a separate forge repository with its own history and its own head commit, so its projection is derivable in isolation and provable in isolation: fetch that repository, rebuild its records, compare against that repository's head. Nothing about that requires knowing the state of any other repository, and a design that made it require so would be inventing a dependency the source does not have.</p>
|
||||
<p>It also bounds the blast radius, which is what makes decision 7's "routine" achievable. A rebuild that can only run fleet-wide is an operation nobody runs casually, and therefore an operation nobody runs — which is precisely how <code>ADR-010</code>'s never-executed reconstruction claim came to be believed. Scoped to one repository, a rebuild is small enough to be ordinary, and ordinary is the only state in which it stays known-working.</p>
|
||||
<p>It is also the only form that composes with the refusals below. A repository holding projection-only records can be held back and dispositioned while every healthy repository around it is rebuilt; an all-or-nothing reset is blocked entirely by a single bad repository, which in practice means it is blocked permanently.</p>
|
||||
<p>With contributors, per-repository scope is what keeps one person's rebuild from touching another person's records.</p>
|
||||
<p>The fleet-wide form must therefore be a loop over the per-repository form, sharing one implementation. The rarely-used dangerous path and the frequently used safe path must be the same code, so the rare one is exercised by the common one rather than trusted on the strength of never having been run.</p>
|
||||
<ul><li><strong>Idempotent and verifiable.</strong> A reset followed by a reset produces the same projection, and the result can be compared against the forge to show it matches. Derived identifiers (<code>ADR-007</code>) are what make this possible: the same commit yields the same record identities every time.</li></ul>
|
||||
<p><strong>Retirement must be visible, not silent.</strong> A retired record states that the forge no longer derives it, and remains inspectable — including from whatever hub-native history is still attached. A record that merely disappears from a listing is indistinguishable from one that was never there.</p>
|
||||
<p><strong>Reset does not restore the preliminary overlay.</strong> Overlay records exist precisely because the forge does not hold them, so a rebuild from the forge cannot reproduce them and must not pretend to. Reset therefore discards preliminary state, and must say so plainly before it runs.</p>
|
||||
<p><strong>A reset must refuse when records exist only in the projection.</strong> If the hub holds records with no counterpart in the forge, rebuilding destroys them. That is not hypothetical: as of 2026-08-25, 111 work records existed only in a retired local database, and a rebuild at that moment would have erased them. The reset path must detect that condition and stop, naming what would be lost, rather than proceed and report success.</p>
|
||||
<p>The refusal is evaluated per repository, in keeping with the scope above. A repository whose records all exist in the forge is rebuildable regardless of what any other repository holds, and a fleet-wide run must skip and report the repositories it refuses rather than abort the whole pass. Otherwise one unresolved repository blocks reconstruction everywhere, and the capability decays back into the untested assumption this decision exists to prevent.</p>
|
||||
<p><strong>8. Formal git review stays optional.</strong> Deriving from the default branch gives a shared baseline without requiring pull requests. Review can be adopted per repository where it earns its keep; this ADR neither mandates nor forbids it.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><strong>Positive.</strong> The hub becomes provably a projection: reconcilable on demand against its source, and therefore knowable to be one. Truth becomes checkable by anyone, from anywhere, without a clone. Multiple contributors share one baseline instead of overwriting each other's views. Provenance becomes auditable — every record can name its commit. The "push then delete the working copy" case simply works. The distinction between committed and uncommitted work becomes visible in the model rather than a matter of who ran which command last.</p>
|
||||
<p><strong>Negative.</strong> Git becomes load-bearing for the hub: Forgejo availability now affects projection freshness. A derive loop needs a cadence, and freshness becomes a property to monitor rather than assume. Unpushed work becomes explicitly second-class — which is its honest status, but it will feel like a restriction to a solo developer used to local-first behaviour.</p>
|
||||
<p><strong>Migration.</strong> The <code>sweep</code> hostPath — central reading a node-local clone tree — is a half-measure toward this decision and was disabled pending "governed remote reconciliation" while railiance01 checkouts still targeted Gitea. As of 2026-08-25 all 79 node checkouts track Forgejo, so that stated blocker has cleared. Sweep should be evaluated as a stepping stone or retired in favour of a direct forge fetch, not left dormant with an obsolete justification.</p>
|
||||
<p><strong>Unresolved.</strong> This ADR does not settle the derive cadence, whether central clones or uses the Forgejo API, how preliminary records are surfaced in the dashboard and MCP, or what happens to a preliminary record whose commit never arrives. Nor does it settle how hub-native records — progress events, decisions, inbox messages, which <code>ADR-010</code> decision 4 classes as originating in the hub — survive a reset. They are not forge-derived and must not be destroyed by a rebuild of forge-derived state; the boundary needs drawing before reset is built. Those belong to implementation.</p>
|
||||
</section>
|
||||
<section id="relationship-to-prior-decisions"><h2>Relationship to prior decisions</h2>
|
||||
<ul><li><strong><code>ADR-001</code></strong> — unchanged. Work still originates as repository files; this ADR says which copy of them the hub reads.</li><li><strong><code>ADR-010</code></strong> — decisions 1 and 5 are sharpened, not reversed: central still derives and still does not accept pushes of derived state. Decision 6's "preliminary" gains a mechanism. The local-cache-versus-database framing stands.</li><li><strong><code>ADR-003</code></strong> — partially superseded. Decision 2 composes fingerprints from filesystem <code>mtime</code>, which is a property of one workstation rather than of the source; under decision 1 here the input is the commit. Decision 5 already stated the rebuild principle correctly but had never been exercised; decision 7 here makes it an operation with a source, a scope and a verification.</li><li><strong><code>ADR-007</code></strong> — derived identifiers become more valuable here: a forge-derived projection and a preliminary overlay compute the same identifier for the same record, so absorbing an overlay entry needs no reconciliation.</li></ul>
|
||||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li><code>ADR-001</code> — workplans originate as repo files; hub is a read model</li><li><code>ADR-010</code> — hub authority, local cache, and the two kinds of hub data</li><li><code>ADR-007</code> — identifier uniqueness and derived identifiers</li><li><code>CUST-WP-0067</code> — hub target resolution; retired the impersonating local instance</li><li><code>CUST-WP-0068</code> — cache-only work-record recovery; surfaced the stale <code>git_fingerprint</code> and the duplicate registrations</li><li>Verification, 2026-08-25: central pod holds no repository files; <code>sweep</code> disabled; 117 repositories record a laptop path; <code>the-custodian</code> <code>git_fingerprint</code> is the initial commit while <code>last_state_synced_at</code> is current</li></ul>
|
||||
</section><footer><span>CUST-ADR-012 · 1.0 · accepted</span><span>the-custodian · canon/architecture/adr-012-projection-source-and-preliminary-overlay.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="6475a2e1ca590682888b77ce32ea056fd02e272a">
|
||||
<meta name="policy-source-digest" content="b5e8582459f546ae789ad5fd62f458454aa19997b520e32b6b9f792d6af55987">
|
||||
<title>What the Hub Projects</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-012</span> <span class="stat">accepted · 1.0</span> <span>the-custodian</span> <span>reviewed 2026-08-25</span><span>generated from canonical source — do not edit</span></div><h1>What the Hub Projects</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-012-projection-source-and-preliminary-overlay.md · 6475a2e1ca590682888b77ce32ea056fd02e272a</code></p><p class="sub">Review due: 2027-02-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#relationship-to-prior-decisions"><span class="n">·</span>Relationship to prior decisions</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p><strong>Accepted 2026-08-25</strong> by Bernd Worsch. Supersedes <code>ADR-010</code> decision 1's phrase "authoritative as a reading of the repositories" by making the reading concrete, and implements decision 6's unbuilt notion of "preliminary".</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>ADR-001</code> says work originates as repository files and the hub is a read model. <code>ADR-010</code> says central is authoritative <em>as a reading of the repositories</em>. Both leave one question unanswered, and it turns out to be the load-bearing one:</p>
|
||||
<div class="rule-quote"><p><strong>Which</strong> repository files? The forge, or a working copy?</p></div>
|
||||
<p>Verified on 2026-08-25, the answer today is <strong>neither</strong>.</p>
|
||||
<p>Central runs one FastAPI pod on railiance01 with a CNPG database. Inspected from inside, it holds <code>/app</code> — its own source — and nothing else. No clone tree, no repository mount; the <code>sweep</code> hostPath that would give it one is disabled. <strong>The hub never reads a repository.</strong> It cannot see the forge, and it cannot see a workstation.</p>
|
||||
<p>What actually happens is the inverse of the ADR language: <code>fix-consistency</code> and <code>registrar-reconcile</code> run <em>on a workstation</em>, read files in <code>/home/<user>/<repo></code>, and POST the result over a tunnel. The <code>host_paths</code> field records this plainly — 117 repositories carry a path on <code>bnt-lap001</code>, a laptop.</p>
|
||||
<p>So the projection derives from <strong>whichever checkout most recently ran the sync</strong>. That is a third thing, distinct from both the forge and any particular working copy, and it is written down nowhere.</p>
|
||||
<h3>Why this is not academic</h3>
|
||||
<p>Every failure investigated under <code>CUST-WP-0067</code> and <code>CUST-WP-0068</code> is downstream of it:</p>
|
||||
<ul><li>A local hub and central both served port 8000, and every default reached the local one for seven weeks. Neither could be distinguished by what it projected.</li><li>50 repository records pointed at a retired forge, so the sync could not match the checkout and silently registered nothing.</li><li>Duplicate registrations accumulated: the same file registered twice under two identifiers, because two environments each believed they were projecting it.</li><li><code>git_fingerprint</code> — the field that should say which commit a projection reflects — still holds the <em>initial commit</em> for <code>the-custodian</code>, while <code>last_state_synced_at</code> is minutes old. There is no reliable way to ask central what state it is a projection <em>of</em>.</li></ul>
|
||||
<h3>The forcing function</h3>
|
||||
<p>With one contributor, "last writer wins" is invisible, because there is only one writer. With several, each pushes a projection of a different repository state into one shared read model, and each will look locally consistent while disagreeing with the others. That is the peer-database problem <code>ADR-010</code> diagnosed, relocated one layer up and made harder to see.</p>
|
||||
<p>The window to fix this cheaply is before the second contributor, not after.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>1. The forge is the projection source.</strong> Central derives its projection from Forgejo — the pushed state of the default branch — not from any working copy. "Authoritative as a reading of the repositories" now names a specific reading: what the forge holds.</p>
|
||||
<p>Forgejo runs in the same cluster as central, so this is an in-cluster fetch. No tunnel, no cross-network credentials, and central already carries a <code>git</code> binary.</p>
|
||||
<p><strong>2. Every projected record carries the commit it derived from.</strong> A projection without provenance cannot be audited, and the existing <code>git_fingerprint</code> proves the point by being wrong and silent about it. Records must reference the commit that produced them, and a projection whose commit is older than the forge's head must be visibly stale rather than quietly current.</p>
|
||||
<p><strong>3. Unpushed local work is a preliminary overlay, never the baseline.</strong> A working copy may contribute records the forge does not yet hold. They are marked preliminary, attributed to their environment, and never silently merged into the derived baseline. When the commit that carries them reaches the forge, the baseline absorbs them and the overlay entry retires.</p>
|
||||
<p>This implements <code>ADR-010</code> decision 6, which named "preliminary" but left it unbuilt.</p>
|
||||
<p><strong>4. The overlay is a label, not a second store.</strong> Preliminary records live in the same projection, flagged. They are not a parallel database and not a local hub. Every local projection this ecosystem has had was eventually mistaken for authority; the defence is that there is nothing separate to mistake.</p>
|
||||
<p><strong>5. A working copy is a workspace, not a source of authority.</strong> Deleting a checkout after pushing must lose nothing and invalidate nothing. Validation of a projection must not require a local clone of the repository being validated.</p>
|
||||
<p><strong>6. Push-based sync is retired as the primary path.</strong> Workstation tooling stops being how records reach central. It may continue to <em>propose</em> preliminary records under decision 3, but the baseline is derived, not received. This closes the gap where "central derives" was policy while "the laptop pushes" was practice.</p>
|
||||
<p><strong>7. The projection must be resettable from the forge, as a routine operation.</strong> There must be a supported way to reconcile the projection — for one repository or for the whole fleet — against what Forgejo holds: create what is missing, update what differs, and <strong>retire what no longer derives</strong>.</p>
|
||||
<div class="rule-quote"><p><strong>Amended 2026-08-25, before implementation.</strong> This decision originally said <em>discard and rebuild</em>. That operation cannot exist, and the reason is a good one. <code>progress_events</code>, <code>tasks</code>, <code>decisions</code> and <code>review_contracts</code> all reference workplans with <code>ON DELETE RESTRICT</code>, and 1067 of 1075 workplans carry at least one such reference. Deleting a workplan would mean deleting the hub-native records attached to it — which <code>ADR-010</code> decision 4 forbids, and which the schema refuses to allow. The database is enforcing this ADR's own boundary one layer down. A progress event recording work on a workplan is a fact that happened; destroying it to tidy a derived projection would be losing hub-native truth to fix a derived-state problem, which is exactly backwards. Retirement achieves what reset is for — the projection converges on the forge, and records that no longer derive stop appearing as live work — without erasing anything that only the hub holds. Reset reconciles; it does not destroy.</p></div>
|
||||
<p>This is the decision that makes the others checkable rather than merely stated. A read model that cannot be rebuilt from its source is not a projection; it is a database with a projection's reputation, and the difference only becomes visible on the day someone needs to rebuild it. <code>ADR-010</code> decision 2 already asserts that a cache "may be discarded and reconstructed from the repositories at any time" — that claim has never been executed, and an untested rebuild path is an assumption, not a capability.</p>
|
||||
<p>Three properties make it real rather than ceremonial:</p>
|
||||
<ul><li><strong>Routine, not emergency.</strong> It should be run deliberately and often enough that it is known to work, not discovered under pressure. A reset that has never been performed is indistinguishable from one that does not work.</li><li><strong>Per repository is the unit.</strong> Not a convenience over a fleet-wide operation — the repository <em>is</em> the unit of reconstruction, and the fleet-wide form is defined as iteration over it.</li></ul>
|
||||
<p>This follows from the source. Each repository is a separate forge repository with its own history and its own head commit, so its projection is derivable in isolation and provable in isolation: fetch that repository, rebuild its records, compare against that repository's head. Nothing about that requires knowing the state of any other repository, and a design that made it require so would be inventing a dependency the source does not have.</p>
|
||||
<p>It also bounds the blast radius, which is what makes decision 7's "routine" achievable. A rebuild that can only run fleet-wide is an operation nobody runs casually, and therefore an operation nobody runs — which is precisely how <code>ADR-010</code>'s never-executed reconstruction claim came to be believed. Scoped to one repository, a rebuild is small enough to be ordinary, and ordinary is the only state in which it stays known-working.</p>
|
||||
<p>It is also the only form that composes with the refusals below. A repository holding projection-only records can be held back and dispositioned while every healthy repository around it is rebuilt; an all-or-nothing reset is blocked entirely by a single bad repository, which in practice means it is blocked permanently.</p>
|
||||
<p>With contributors, per-repository scope is what keeps one person's rebuild from touching another person's records.</p>
|
||||
<p>The fleet-wide form must therefore be a loop over the per-repository form, sharing one implementation. The rarely-used dangerous path and the frequently used safe path must be the same code, so the rare one is exercised by the common one rather than trusted on the strength of never having been run.</p>
|
||||
<ul><li><strong>Idempotent and verifiable.</strong> A reset followed by a reset produces the same projection, and the result can be compared against the forge to show it matches. Derived identifiers (<code>ADR-007</code>) are what make this possible: the same commit yields the same record identities every time.</li></ul>
|
||||
<p><strong>Retirement must be visible, not silent.</strong> A retired record states that the forge no longer derives it, and remains inspectable — including from whatever hub-native history is still attached. A record that merely disappears from a listing is indistinguishable from one that was never there.</p>
|
||||
<p><strong>Reset does not restore the preliminary overlay.</strong> Overlay records exist precisely because the forge does not hold them, so a rebuild from the forge cannot reproduce them and must not pretend to. Reset therefore discards preliminary state, and must say so plainly before it runs.</p>
|
||||
<p><strong>A reset must refuse when records exist only in the projection.</strong> If the hub holds records with no counterpart in the forge, rebuilding destroys them. That is not hypothetical: as of 2026-08-25, 111 work records existed only in a retired local database, and a rebuild at that moment would have erased them. The reset path must detect that condition and stop, naming what would be lost, rather than proceed and report success.</p>
|
||||
<p>The refusal is evaluated per repository, in keeping with the scope above. A repository whose records all exist in the forge is rebuildable regardless of what any other repository holds, and a fleet-wide run must skip and report the repositories it refuses rather than abort the whole pass. Otherwise one unresolved repository blocks reconstruction everywhere, and the capability decays back into the untested assumption this decision exists to prevent.</p>
|
||||
<p><strong>8. Formal git review stays optional.</strong> Deriving from the default branch gives a shared baseline without requiring pull requests. Review can be adopted per repository where it earns its keep; this ADR neither mandates nor forbids it.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><strong>Positive.</strong> The hub becomes provably a projection: reconcilable on demand against its source, and therefore knowable to be one. Truth becomes checkable by anyone, from anywhere, without a clone. Multiple contributors share one baseline instead of overwriting each other's views. Provenance becomes auditable — every record can name its commit. The "push then delete the working copy" case simply works. The distinction between committed and uncommitted work becomes visible in the model rather than a matter of who ran which command last.</p>
|
||||
<p><strong>Negative.</strong> Git becomes load-bearing for the hub: Forgejo availability now affects projection freshness. A derive loop needs a cadence, and freshness becomes a property to monitor rather than assume. Unpushed work becomes explicitly second-class — which is its honest status, but it will feel like a restriction to a solo developer used to local-first behaviour.</p>
|
||||
<p><strong>Migration.</strong> The <code>sweep</code> hostPath — central reading a node-local clone tree — is a half-measure toward this decision and was disabled pending "governed remote reconciliation" while railiance01 checkouts still targeted Gitea. As of 2026-08-25 all 79 node checkouts track Forgejo, so that stated blocker has cleared. Sweep should be evaluated as a stepping stone or retired in favour of a direct forge fetch, not left dormant with an obsolete justification.</p>
|
||||
<p><strong>Unresolved.</strong> This ADR does not settle the derive cadence, whether central clones or uses the Forgejo API, how preliminary records are surfaced in the dashboard and MCP, or what happens to a preliminary record whose commit never arrives. Nor does it settle how hub-native records — progress events, decisions, inbox messages, which <code>ADR-010</code> decision 4 classes as originating in the hub — survive a reset. They are not forge-derived and must not be destroyed by a rebuild of forge-derived state; the boundary needs drawing before reset is built. Those belong to implementation.</p>
|
||||
</section>
|
||||
<section id="relationship-to-prior-decisions"><h2>Relationship to prior decisions</h2>
|
||||
<ul><li><strong><code>ADR-001</code></strong> — unchanged. Work still originates as repository files; this ADR says which copy of them the hub reads.</li><li><strong><code>ADR-010</code></strong> — decisions 1 and 5 are sharpened, not reversed: central still derives and still does not accept pushes of derived state. Decision 6's "preliminary" gains a mechanism. The local-cache-versus-database framing stands.</li><li><strong><code>ADR-003</code></strong> — partially superseded. Decision 2 composes fingerprints from filesystem <code>mtime</code>, which is a property of one workstation rather than of the source; under decision 1 here the input is the commit. Decision 5 already stated the rebuild principle correctly but had never been exercised; decision 7 here makes it an operation with a source, a scope and a verification.</li><li><strong><code>ADR-007</code></strong> — derived identifiers become more valuable here: a forge-derived projection and a preliminary overlay compute the same identifier for the same record, so absorbing an overlay entry needs no reconciliation.</li></ul>
|
||||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li><code>ADR-001</code> — workplans originate as repo files; hub is a read model</li><li><code>ADR-010</code> — hub authority, local cache, and the two kinds of hub data</li><li><code>ADR-007</code> — identifier uniqueness and derived identifiers</li><li><code>CUST-WP-0067</code> — hub target resolution; retired the impersonating local instance</li><li><code>CUST-WP-0068</code> — cache-only work-record recovery; surfaced the stale <code>git_fingerprint</code> and the duplicate registrations</li><li>Verification, 2026-08-25: central pod holds no repository files; <code>sweep</code> disabled; 117 repositories record a laptop path; <code>the-custodian</code> <code>git_fingerprint</code> is the initial commit while <code>last_state_synced_at</code> is current</li></ul>
|
||||
</section><footer><span>CUST-ADR-012 · 1.0 · accepted</span><span>the-custodian · canon/architecture/adr-012-projection-source-and-preliminary-overlay.md · 6475a2e1ca590682888b77ce32ea056fd02e272a</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-digest" content="69f463795bdf1769c11415e9c8afa170d6fe62f04a554f7573f269000b5c4b08">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="1169f0c1f485a2bb7241a8f64f3167c060c04f83341b12586fd9be5c2b3693f8">
|
||||
<title>Workplan Identity Uniqueness, Single Registrar, and Repo Worker Topology</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-007</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>Workplan Identity Uniqueness, Single Registrar, and Repo Worker Topology</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#migration-needs-a-separate-ruling"><span class="n">·</span>Migration — needs a separate ruling</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-007</span> <span class="stat">accepted · accepted-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Workplan Identity Uniqueness, Single Registrar, and Repo Worker Topology</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#migration-needs-a-separate-ruling"><span class="n">·</span>Migration — needs a separate ruling</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-17. Identifier uniqueness, the registrar model, lifecycle protection, and worker topology are settled.</p>
|
||||
<p><strong>Remediation of existing collisions (§ Migration) remains an open ruling.</strong> It is disruptive, touches six repositories, and no active work depends on it — all five duplicated identifiers are <code>finished</code>.</p>
|
||||
</section>
|
||||
|
|
@ -228,6 +228,7 @@ RAILIANCE-WP-0016 railiance-platform ×2, railiance-apps</pre>
|
|||
<p><em>Rejected.</em> Collapsing to a single shared database (workstation hubs pointing at the production database) would also make identifiers consistent by construction, but it eliminates offline capability — cutting directly against <code>STATE-WP-0068</code> (offline write buffer and edge relay) — and couples all local work to tunnel availability.</p>
|
||||
<p><em>Ownership.</em> Both the interim guard and the derivation belong to <code>repo-manager</code> under decision <code>747011c6</code>, which already places file-backed record indexing and reconciliation there. Building either in <code>state-hub</code> would invest in a component being retired under <code>STATE-WP-0079</code>.</p>
|
||||
<p>Migration scope for C2: <strong>758 workplan files</strong> across the fleet currently carry these fields.</p>
|
||||
<div class="rule-quote"><p><strong>Amended 2026-08-21 — derivation scope for C2.</strong> <code>ADR-011</code> decision 7 keys derivation on <code>(namespace, identifier)</code>, which separates <em>forks</em>. It does not separate collisions inside one namespace, and the ecosystem's posture is <code>N1</code> — a single implied namespace. A fleet scan on 2026-08-21 found <strong>20 reused identifiers across 48 files</strong>, all within that one namespace, so the prerequisite above is not satisfied by <code>ADR-011</code> alone. Ruled: <strong>C2 derives only for live records; archived records keep their minted identifiers, frozen.</strong> This is what reconciles § Migration option 2 — under which historical files keep colliding identifiers — with the uniqueness derivation requires. Of the 20, only <strong>five</strong> collide among live files, so the remediation surface is 11 files rather than 48. Two consequences follow, and both are load-bearing: 1. <strong>Un-archiving a record with a frozen identifier is a collision hazard.</strong> A record returning to live status must be checked against the live namespace before it is re-derived, and renumbered if it clashes. 2. <strong>Derivation is not retroactive.</strong> Existing live records keep their minted UUIDs until they are re-derived deliberately; C2 changes provenance for new and re-registered records, not the whole corpus at once. Rejected: treating a <em>repository</em> as the namespace. That would make the collisions vanish by construction, but it redefines the term <code>ADR-011</code> decision 1 fixes as "a fleet instance, a client deployment, an autonomous domain", and <code>ADR-011</code> decision 9 warns specifically against reading N-plane movement into claims it does not support.</p></div>
|
||||
<p><strong>3. Lifecycle status is not automatically promoted.</strong> An automated normalization pass may report drift; it may not move a workplan from <code>proposed</code> to <code>active</code>. <code>proposed</code> means awaiting human review, and an automation that promotes it destroys the meaning of the review gate.</p>
|
||||
<p><strong>4. Repository manipulation is performed by a worker agent in that repository.</strong> This is the default topology.</p>
|
||||
<ul><li>A worker acting in repo X owns changes to repo X.</li><li>Multiple independent top-level workers inside a single repository are an <strong>exception</strong>, requiring an explicit reason, not a routine mode of operation.</li><li>Concurrent independent writers are what turned a two-registrar bug into repeated git divergence.</li></ul>
|
||||
|
|
@ -275,4 +276,4 @@ CUST-WP- the-custodian 50 plans
|
|||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li>Decision <code>747011c6</code> — repository standards belong to Repo Manager</li><li>ADR-001 — workplans originate as repo files; hub is a read model</li><li><code>RMGR-WP-0004</code> — repository standards conformance and governed scaffolding</li><li><code>STATE-WP-0080</code> — register scaffolding handoff</li><li>Fleet scan 2026-08-16: 955 hub workplans, 525 parseable identifiers, 3 reused prefixes, 5 reused identifiers</li></ul>
|
||||
</section><footer><span>CUST-ADR-007 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
</section><footer><span>CUST-ADR-007 · accepted-2 · accepted</span><span>the-custodian · canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,279 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d3c6f13d7aed9784b6f31c23d2b2777668493ba5">
|
||||
<meta name="policy-source-digest" content="1169f0c1f485a2bb7241a8f64f3167c060c04f83341b12586fd9be5c2b3693f8">
|
||||
<title>Workplan Identity Uniqueness, Single Registrar, and Repo Worker Topology</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-007</span> <span class="stat">accepted · accepted-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Workplan Identity Uniqueness, Single Registrar, and Repo Worker Topology</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#migration-needs-a-separate-ruling"><span class="n">·</span>Migration — needs a separate ruling</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-17. Identifier uniqueness, the registrar model, lifecycle protection, and worker topology are settled.</p>
|
||||
<p><strong>Remediation of existing collisions (§ Migration) remains an open ruling.</strong> It is disruptive, touches six repositories, and no active work depends on it — all five duplicated identifiers are <code>finished</code>.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>On 2026-08-16, <code>STATE-WP-0080</code> was found registered <strong>twice, in two different databases</strong>, with two different workplan UUIDs and two disjoint sets of task UUIDs:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Registrar</th><th>Workplan UUID</th><th>Reachable at <code>127.0.0.1:8000</code></th></tr></thead><tbody><tr><td>Workstation hub (<code>make api</code>, local postgres)</td><td><code>03f38314</code></td><td>yes</td></tr><tr><td>A second instance, over a second database</td><td><code>bbfce36a</code></td><td><strong>404</strong></td></tr></tbody></table></div>
|
||||
<p>The second instance is <em>not</em> identifiable from the commit author. <code>custodian-sync@railiance.local</code> is a hardcoded default git identity in <code>scripts/consistency_check.py:1899</code> (<code>GIT_SYNC_USER_NAME</code> / <code>GIT_SYNC_USER_EMAIL</code>), so every machine running <code>fix-consistency</code> commits under that name. The discriminator is the timezone: sync commits appear under both <code>+0000</code> and <code>+0200</code> (the workstation's offset), which is independent evidence of two machines writing to one repository.</p>
|
||||
<p>Both write their IDs into the same git-tracked workplan file, so each sync overwrites the other's IDs and the file flip-flops on every round trip. The same commit (<code>ff909e1</code>, "renormalize lifecycle state [auto]") also promoted the workplan <code>proposed</code> → <code>active</code> without human review.</p>
|
||||
<p>A fleet scan of 955 hub workplans (525 with parseable <code>PREFIX-WP-NNNN</code> identifiers) found this is not an isolated incident. Two distinct identity defects are live:</p>
|
||||
<p><strong>Prefix reuse across repositories:</strong></p>
|
||||
<pre>CUST-WP- → state-hub, the-custodian
|
||||
RAIL-BS-WP- → railiance-bootstrap, railiance-cluster
|
||||
RAILIANCE-WP- → railiance-apps, railiance-forge, railiance-platform, railiance-telemetry</pre>
|
||||
<p><code>PRJ-WP-</code> is a latent fourth: <code>statehub register</code> derives it from the <code>prj-</code> flavor marker, so every project repository would collide (see <code>RMGR-WP-0004</code>).</p>
|
||||
<p><strong>Running-number reuse:</strong></p>
|
||||
<pre>CUST-WP-0000 the-custodian ×2
|
||||
CUST-WP-0010 the-custodian ×2
|
||||
CUST-WP-0045 the-custodian ×2
|
||||
RAILIANCE-WP-0015 railiance-platform, railiance-apps
|
||||
RAILIANCE-WP-0016 railiance-platform ×2, railiance-apps</pre>
|
||||
<p><code>RAILIANCE-WP-0015</code> is actively cited in operational memory as the cnpg-backup gate in <code>railiance-apps</code>; a second record of the same name in <code>railiance-platform</code> makes that citation ambiguous.</p>
|
||||
<p>The <code>C-26</code> consistency check already flags prefix nonconformance <em>within</em> a repo against a canonical prefix, but nothing enforces uniqueness <em>across</em> repos, and nothing prevents number reuse.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>1. A workplan identifier is globally unique.</strong> <code>PREFIX-WP-NNNN</code> names exactly one workplan across the entire fleet, for all time.</p>
|
||||
<div class="rule-quote"><p><strong>Amended 2026-08-17 by <code>ADR-011</code> decision 2.</strong> Uniqueness and forward-only allocation are <strong>namespace-scoped</strong>, not global; global identity is the pair <code>(namespace, identifier)</code>, written <code>PREFIX-WP-NNNN@namespace</code> when foreign. Global sequential allocation would require a central coordinator — the exact dependency federation must survive. Everything below holds unchanged <strong>within</strong> a namespace, which is where all current work sits.</p></div>
|
||||
<ul><li>A workplan prefix is owned by exactly one repository. No two repositories may use the same prefix.</li><li>A running number is never reused within a prefix, including after a workplan is cancelled, archived, or deleted. Numbers are allocated forward only.</li><li>Prefixes are derived from the <em>project or repository identity</em>, never from a flavor marker or category (<code>PRJ-WP-</code> is invalid by construction).</li></ul>
|
||||
<p><strong>2. Hub identifiers stored in repository files must be derivable, not database-local.</strong> The defect is structural: a database-local key is stored in a shared artifact, so each database overwrites the other's value on every sync. It also inverts ADR-001 — a file carrying a hub's private key is the file holding hub state.</p>
|
||||
<p><em>Target state (C2).</em> <code>state_hub_workstream_id</code> and <code>state_hub_task_id</code> become <strong>deterministic</strong>: UUIDv5 derived from the workplan identifier. Per <code>ADR-011</code> decision 3 the derivation input is the pair <code>(namespace, identifier)</code>, not the identifier alone — deriving from the identifier alone would make two forks holding unrelated work under the same number compute the same UUID. Every instance computes the same value independently, writeback becomes idempotent, and any number of hub instances may coexist without coordination. The field shape is unchanged, so consumers keep working; only the provenance of the value changes.</p>
|
||||
<p>This has a hard prerequisite: deterministic derivation from a <strong>non-unique</strong> identifier would manufacture collisions rather than remove them. Two project repos sharing <code>PRJ-WP-</code> would compute the same UUID for different workplans. Decision 1 must therefore be enforced before derivation ships.</p>
|
||||
<p><em>Interim state (A).</em> Until derivation lands, exactly one instance writes hub identifiers into repository files. Other instances may read, project, and serve, but must not mint workplan or task UUIDs into git-tracked files.</p>
|
||||
<div class="rule-quote"><p><strong>Corrected 2026-08-17, superseded by <code>ADR-010</code> decisions 1–3.</strong> This decision originally described workstation hubs as "development read replicas". That was wrong on both counts: the workstation instance was not a replica, and it was the <em>larger</em> of the two, holding 306 more workplans than the primary. The two instances were peer databases. <code>ADR-010</code> establishes the central hub as authoritative and local instances as rebuildable caches, which is what makes this interim rule coherent.</p></div>
|
||||
<p>The interim is policy, enforced by discipline, and it has a real cost: registration requires connectivity to the registrar, so disconnected work cannot register. That cost is accepted only until C2 removes the need for it, at which point the number of hub instances becomes an availability choice rather than a correctness constraint.</p>
|
||||
<p><em>Rejected.</em> Collapsing to a single shared database (workstation hubs pointing at the production database) would also make identifiers consistent by construction, but it eliminates offline capability — cutting directly against <code>STATE-WP-0068</code> (offline write buffer and edge relay) — and couples all local work to tunnel availability.</p>
|
||||
<p><em>Ownership.</em> Both the interim guard and the derivation belong to <code>repo-manager</code> under decision <code>747011c6</code>, which already places file-backed record indexing and reconciliation there. Building either in <code>state-hub</code> would invest in a component being retired under <code>STATE-WP-0079</code>.</p>
|
||||
<p>Migration scope for C2: <strong>758 workplan files</strong> across the fleet currently carry these fields.</p>
|
||||
<div class="rule-quote"><p><strong>Amended 2026-08-21 — derivation scope for C2.</strong> <code>ADR-011</code> decision 7 keys derivation on <code>(namespace, identifier)</code>, which separates <em>forks</em>. It does not separate collisions inside one namespace, and the ecosystem's posture is <code>N1</code> — a single implied namespace. A fleet scan on 2026-08-21 found <strong>20 reused identifiers across 48 files</strong>, all within that one namespace, so the prerequisite above is not satisfied by <code>ADR-011</code> alone. Ruled: <strong>C2 derives only for live records; archived records keep their minted identifiers, frozen.</strong> This is what reconciles § Migration option 2 — under which historical files keep colliding identifiers — with the uniqueness derivation requires. Of the 20, only <strong>five</strong> collide among live files, so the remediation surface is 11 files rather than 48. Two consequences follow, and both are load-bearing: 1. <strong>Un-archiving a record with a frozen identifier is a collision hazard.</strong> A record returning to live status must be checked against the live namespace before it is re-derived, and renumbered if it clashes. 2. <strong>Derivation is not retroactive.</strong> Existing live records keep their minted UUIDs until they are re-derived deliberately; C2 changes provenance for new and re-registered records, not the whole corpus at once. Rejected: treating a <em>repository</em> as the namespace. That would make the collisions vanish by construction, but it redefines the term <code>ADR-011</code> decision 1 fixes as "a fleet instance, a client deployment, an autonomous domain", and <code>ADR-011</code> decision 9 warns specifically against reading N-plane movement into claims it does not support.</p></div>
|
||||
<p><strong>3. Lifecycle status is not automatically promoted.</strong> An automated normalization pass may report drift; it may not move a workplan from <code>proposed</code> to <code>active</code>. <code>proposed</code> means awaiting human review, and an automation that promotes it destroys the meaning of the review gate.</p>
|
||||
<p><strong>4. Repository manipulation is performed by a worker agent in that repository.</strong> This is the default topology.</p>
|
||||
<ul><li>A worker acting in repo X owns changes to repo X.</li><li>Multiple independent top-level workers inside a single repository are an <strong>exception</strong>, requiring an explicit reason, not a routine mode of operation.</li><li>Concurrent independent writers are what turned a two-registrar bug into repeated git divergence.</li></ul>
|
||||
<p><strong>5. Project (<code>prj-</code>) repositories may act across their participating repositories.</strong> When work is governed by a project repo, its tasks may direct changes across every repository the project names, through the project's work agent, where that is more efficient than delegating.</p>
|
||||
<p>This is a deliberate, scoped exception to decision 4: the project repo already owns cross-repo sequencing and its <code>SCOPE.md</code> names its participants, so its authority is declared rather than ad hoc. It does not license a worker in an arbitrary repository to reach into others.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><strong>Positive.</strong> Workplan identifiers become citable without qualification. Hub IDs stop flip-flopping in git. The <code>proposed</code> status regains meaning. Cross-repo authority becomes something a repository declares rather than something any session assumes.</p>
|
||||
<p><strong>Negative.</strong> Existing collisions must be remediated (see below), which is disruptive. Workstation sessions lose the ability to register workplans directly and must route through the registrar or a worker in the owning repo. Prefix allocation needs a fleet-level registry, which is new machinery.</p>
|
||||
<p><strong>Enforcement.</strong> Prefix ownership, uniqueness, and forward-only numbering are repository standards, so they belong to Repo Manager under decision <code>747011c6</code> (<code>RMGR-WP-0004</code>), not to a hub. Canon defines the rule; Repo Manager checks it.</p>
|
||||
</section>
|
||||
<section id="migration-needs-a-separate-ruling"><h2>Migration — needs a separate ruling</h2>
|
||||
<p>Three prefixes and five identifiers are already colliding. Remediation options, in increasing cost:</p>
|
||||
<ol><li><strong>Freeze and forward-fix.</strong> Accept existing collisions as historical, enforce uniqueness only for new workplans. Cheapest; leaves <code>RAILIANCE-WP-0015</code> permanently ambiguous.</li><li><strong>Renumber the live collisions only.</strong> Fix identifiers that are still cited or active; leave <code>finished</code>/<code>archived</code> duplicates alone.</li><li><strong>Full renaming.</strong> Give <code>railiance-apps</code>, <code>railiance-forge</code>, <code>railiance-platform</code>, <code>railiance-telemetry</code> distinct prefixes, likewise <code>railiance-bootstrap</code>/<code>railiance-cluster</code> and <code>state-hub</code>'s legacy <code>CUST-WP-</code> files. Touches six repositories and every inbound reference.</li></ol>
|
||||
<p><strong>Ruled 2026-08-17: option 2.</strong> Renumber live collisions; leave <code>finished</code>/<code>archived</code> duplicates as historical record.</p>
|
||||
<h3>The live renumber list is empty</h3>
|
||||
<p>Verified against the fleet scan. All five duplicated identifiers are <code>finished</code>:</p>
|
||||
<pre>CUST-WP-0000 the-custodian ×2 finished
|
||||
CUST-WP-0010 the-custodian ×2 finished
|
||||
CUST-WP-0045 the-custodian ×2 finished
|
||||
RAILIANCE-WP-0015 apps, platform finished
|
||||
RAILIANCE-WP-0016 apps, platform ×2 finished</pre>
|
||||
<p>No workplan in a <code>proposed</code>, <code>ready</code>, <code>active</code>, <code>blocked</code>, or <code>backlog</code> state shares an identifier with another. <strong>Option 2 therefore requires no renumbering today.</strong> Historical duplicates stay, including the <code>RAILIANCE-WP-0015</code> ambiguity between <code>railiance-apps</code> and <code>railiance-platform</code>; citations of it must name the repository.</p>
|
||||
<h3>The structural cause is not historical</h3>
|
||||
<p>Option 2 governs <em>remediation</em>. It does not exempt anything from decision 1, which is accepted canon: one prefix, one repository, forward-only numbering.</p>
|
||||
<p>Three shared prefixes are still in use across seven repositories, and each is a <strong>single number line being allocated from concurrently</strong>:</p>
|
||||
<pre>RAIL-BS-WP- bootstrap 8, 9
|
||||
cluster 7, 10, 11, 12, 13, 14
|
||||
|
||||
RAILIANCE-WP- platform 5, 8-17 (16 twice — an internal duplicate)
|
||||
apps 15, 16 <- already collided with platform
|
||||
forge 2
|
||||
telemetry 1
|
||||
|
||||
CUST-WP- the-custodian 50 plans
|
||||
state-hub 4 legacy plans (canonical prefix is STATE-WP)</pre>
|
||||
<p><code>RAIL-BS-</code> and <code>RAILIANCE-</code> are actively growing — <code>RAIL-BS-WP-0014</code> (<code>ready</code>), <code>RAILIANCE-WP-0002</code> (<code>ready</code>), <code>RAILIANCE-WP-0001</code> (<code>proposed</code>), all created 2026-08-11 or later. The <code>RAILIANCE-WP-0015</code>/<code>0016</code> collisions were not a historical accident; they are what concurrent allocation from a shared sequence produces, and it will recur at the next concurrent allocation.</p>
|
||||
<p><code>CUST-</code> is dormant on the <code>state-hub</code> side — four legacy plans, one in <code>backlog</code> — and needs no split, only a prefix-ownership assertion.</p>
|
||||
<h3>Prefix assignments</h3>
|
||||
<p><code>RAIL-BS-WP-</code> is <strong>retired</strong> (2026-08-17). Neither repository keeps it: <code>railiance-cluster</code> adopts <code>RCLUSTER-WP-</code> for active and future plans; <code>railiance-bootstrap</code> adopts <code>RBS-WP-</code> for future plans. Finished and archived files keep <code>RAIL-BS-WP-</code> as historical record, consistent with option 2.</p>
|
||||
<p>Migrating plans keep their running numbers — the prefix changes, the number does not. This preserves traceability and cannot violate forward-only allocation, because neither new prefix has prior history. <code>railiance-bootstrap</code> begins at <code>RBS-WP-0010</code>, above its historical maximum, leaving the lower range free should its finished plans ever be adopted into the new prefix.</p>
|
||||
<p><code>RAILIANCE-WP-</code> should follow the same pattern — retired rather than awarded to one repository, since it names a family rather than a repository and so fails decision 1 for the same reason <code>PRJ-WP-</code> does. Assignment of the four successor prefixes is outstanding.</p>
|
||||
<p>Execution of each rename belongs to a worker in the owning repository under decision 4. <code>RMGR-WP-0004-T09</code> records assignments and the numbering rule; it does not perform renames.</p>
|
||||
<p><strong>Consequence.</strong> Prefix ownership must be assigned for all three shared prefixes before the next workplan is created in the affected repositories. This is forward conformance under decision 1, not migration, and is tracked as <code>RMGR-WP-0004-T09</code>. Renaming the <em>historical</em> files is explicitly not required — that would be option 3, which was rejected.</p>
|
||||
</section>
|
||||
<section id="references"><h2>References</h2>
|
||||
<ul><li>Decision <code>747011c6</code> — repository standards belong to Repo Manager</li><li>ADR-001 — workplans originate as repo files; hub is a read model</li><li><code>RMGR-WP-0004</code> — repository standards conformance and governed scaffolding</li><li><code>STATE-WP-0080</code> — register scaffolding handoff</li><li>Fleet scan 2026-08-16: 955 hub workplans, 525 parseable identifiers, 3 reused prefixes, 5 reused identifiers</li></ul>
|
||||
</section><footer><span>CUST-ADR-007 · accepted-2 · accepted</span><span>the-custodian · canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</span></footer></main></div></div></html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4039c9d1c08c92014ecc0a65dda63cc73ba187bb">
|
||||
<meta name="policy-source-digest" content="64b11785b683cf21ba2aca18e3b8f3301d6070e6a022df6efc722597a8547334">
|
||||
<meta name="policy-source-revision" content="44500fc85cf29d8e9b2ee5c91994032ed3d04e5b">
|
||||
<meta name="policy-source-digest" content="183023ee57bae9c29e726fec5ee0361633fe3a2b182436a57869ae4b2a27af24">
|
||||
<title>Workplans and Work Items Are Repository Artefacts</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,108 +191,67 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-001</span> <span class="stat">accepted · accepted-1</span> <span>the-custodian</span> <span>reviewed 2026-02-28</span><span>generated from canonical source — do not edit</span></div><h1>Workplans and Work Items Are Repository Artefacts</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-001-workplans-as-repo-artefacts.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</code></p><p class="sub">Review due: 2026-08-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#workplan-file-convention"><span class="n">·</span>Workplan File Convention</a></li><li><a href="#rebuild-principle"><span class="n">·</span>Rebuild Principle</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#workplan-closure-protocol"><span class="n">·</span>Workplan Closure Protocol</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted.</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-001</span> <span class="stat">accepted · accepted-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Workplans and Work Items Are Repository Artefacts</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-001-workplans-as-repo-artefacts.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#workplan-closure"><span class="n">·</span>Workplan closure</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#migration"><span class="n">·</span>Migration</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-02-28.</p>
|
||||
<p>Amended 2026-08-31 to distinguish file-backed work records from hub-native records, identify the Forge default branch as the central projection baseline, and align identity, lifecycle, reconciliation, and closure with ADR-007, ADR-010, ADR-011, ADR-012, and the work-record standards. The central decision is unchanged.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>During early State Hub development (v0.1–v0.4), workstreams and tasks were created directly in the PostgreSQL database via MCP bootstrap tools (<code>create_workstream</code>, <code>create_task</code>). This made the database the <strong>origin</strong> of work items — not a cache or index. The pattern was convenient for rapid bootstrapping but is architecturally wrong for a system built on the values of auditability, reversibility, and local-first sovereignty.</p>
|
||||
<p>The trigger for formalising this decision was the creation of the v0.5 workplan ("Dynamic Domains & Multi-Repo") directly in the state-hub database without a corresponding file artefact in any repository.</p>
|
||||
<p>During early State Hub development, workstreams and tasks were created directly in PostgreSQL through bootstrap APIs. This made a database the origin of durable work rather than a projection of repository-owned artefacts. The pattern was convenient, but it made work difficult to audit, review, recover, and carry between generations of tooling.</p>
|
||||
<p>The original decision correctly moved durable workplans and their work items into repositories. It overstated the consequence, however, by saying that the entire Hub database and every item that matters for coordination must be reconstructible from repository files. Later decisions established two different persistence classes:</p>
|
||||
<ul><li><strong>file-backed records</strong>, whose durable meaning originates in a repository;</li><li><strong>hub-native records</strong>, such as append-only progress and runtime facts, whose durable meaning originates in the Hub.</li></ul>
|
||||
<p>Later decisions also established that a central projection reads the pushed default branch in Forgejo. An arbitrary workstation checkout is a workspace, not the shared baseline. Unpushed local work may be represented only as an explicit preliminary overlay.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>Workplans and work items MUST originate as Markdown files in the repository that owns them.</strong> The Custodian State Hub indexes and caches those artefacts but is never their origin.</p>
|
||||
<p>Formally: the state-hub must (theoretically, given sufficient compute and time) be able to <strong>rebuild its full representation</strong> of repositories, their workplans, tasks, decisions, and dependencies by reading only the files in the registered repositories. No information that matters for coordination should exist solely in the database.</p>
|
||||
<h3>Corollaries</h3>
|
||||
<ol><li><strong>Repository is authoritative.</strong> A workplan file is the canonical record. The state-hub database row is a materialized cache of that file.</li></ol>
|
||||
<ol><li><strong>Database is disposable.</strong> Dropping and re-creating the database from registered repository files must produce an equivalent state. The database is an operational convenience, not a primary store.</li></ol>
|
||||
<ol><li><strong>MCP bootstrap tools become index/sync tools.</strong> <code>create_workstream</code> and <code>create_task</code> are acceptable as convenience wrappers only if they write the file first and then register the row. Using them to write DB-only records violates this ADR.</li></ol>
|
||||
<ol><li><strong>The rebuild principle implies a sync mechanism.</strong> There must be a defined path (<code>make sync-workplans</code> or equivalent) by which the state-hub reads workplan files from registered repositories and upserts its database state.</li></ol>
|
||||
<h3>1. File-backed work originates in the owning repository</h3>
|
||||
<p>Workplans and their file-backed work items <strong>MUST originate as repository artefacts in the repository that owns the work</strong>. The Hub indexes and projects those artefacts but is not their origin.</p>
|
||||
<p>This includes the workplan, embedded tasks, declared dependencies, residual handoffs, and durable governed decision artefacts when those are represented as files. No authoritative field of a file-backed record may exist solely in a Hub database.</p>
|
||||
<p>The owning repository is identified by repository identity, not merely by a domain or topic. Domains and topics classify work; they do not own its source.</p>
|
||||
<h3>2. Authority is explicit per persistence class</h3>
|
||||
<p>Every record contract must declare whether the record is file-backed or hub-native. The same record must not be writable as authoritative in both places.</p>
|
||||
<ul><li>For a <strong>file-backed record</strong>, the repository artefact is authoritative and the Hub row is derived state.</li><li>For a <strong>hub-native record</strong>, the Hub is authoritative. Progress events, messages, token events, run history, and similar runtime facts are not made fictional repository records merely to satisfy a rebuild slogan.</li></ul>
|
||||
<p>The word <code>decision</code> is used by more than one subsystem. Architecture decisions and other governed decision artefacts remain files. A runtime decision event may be hub-native only where its schema says so explicitly; it does not replace the governed artefact.</p>
|
||||
<p>This replaces the original blanket rejection of a hybrid architecture. The architecture is hybrid <strong>by declared record class</strong>, never ambiguous within one record.</p>
|
||||
<h3>3. Forge is the central projection baseline</h3>
|
||||
<p>For the shared central view, the source is the pushed default-branch state held by Forgejo, as decided by ADR-012. Every projected record must be attributable to its source repository, path, and commit.</p>
|
||||
<p>A working copy remains the authoring workspace. Unpushed work may appear in the Hub only as an attributed preliminary overlay. It must not silently replace or be presented as the Forge-derived baseline. When its commit reaches Forge, the baseline absorbs it and the overlay retires.</p>
|
||||
<h3>4. Mutations of file-backed state are file-first</h3>
|
||||
<p>Creating or changing a file-backed work record means changing its repository artefact first. A convenience tool or API is conformant only when it performs a governed repository mutation and leaves a reviewable file and commit. Writing only the projected database row is not a durable update.</p>
|
||||
<p>Direct Hub APIs remain valid for hub-native records. They may also provide diagnostics or propose repository patches, but a successful database PATCH is not evidence that a file-backed status changed.</p>
|
||||
<h3>5. The rebuild guarantee applies to the projection, not the whole database</h3>
|
||||
<p>Given a repository and a specific Forge commit, the system must be able to reconcile that repository's file-derived projection so that it is equivalent to the records derived from that commit. Reconciliation must be idempotent, verifiable, and scoped per repository; a fleet operation is iteration over the same per-repository operation.</p>
|
||||
<p>Reconciliation retires file-derived records that no longer derive. It does not delete hub-native history attached to them. It must refuse and report a repository whose records exist only in the projection until those records have an explicit disposition. A Forge rebuild does not reconstruct preliminary overlays and must disclose their retirement before proceeding.</p>
|
||||
<p>Therefore the <strong>file-derived projection is disposable</strong>. The database as a whole is not disposable when it also contains hub-native facts.</p>
|
||||
<h3>6. Identity and lifecycle are delegated contracts</h3>
|
||||
<p>This ADR does not define a second identity or lifecycle schema.</p>
|
||||
<ul><li>Workplan and task identity follow ADR-007 as amended by ADR-011: global identity is namespace-aware, and derivable Hub identifiers are governed by that contract. Existing historical identifiers are grandfathered according to its migration rules.</li><li>Work-record kinds, locations, lifecycle values, and residual handling follow <code>canon/standards/work-record-types_v0.1.md</code> and <code>canon/standards/workplan-terminology-fleet_v0.1.md</code>.</li></ul>
|
||||
<p>New normative text uses <code>workplan</code> and <code>work record</code>. <code>workstream</code> remains only as a metered compatibility term for legacy database and API surfaces.</p>
|
||||
<h3>7. Reconciliation belongs at the repository boundary</h3>
|
||||
<p>Repo Manager owns discovery, parsing, identity checks, and reconciliation for file-backed records. State Hub owns the projection and hub-native records. The implementation may distribute fetch and parse work, but it must preserve that authority boundary.</p>
|
||||
<p>Legacy <code>create_workstream</code>, <code>create_task</code>, workstation-driven <code>sync-workplans</code>, and similarly named commands are not normative interfaces. They are conformant only if their current implementation satisfies decisions 3 and 4; otherwise they are transitional or retired.</p>
|
||||
</section>
|
||||
<section id="workplan-file-convention"><h2>Workplan File Convention</h2>
|
||||
<p>Each workplan lives in a <code>workplans/</code> directory in the repository that owns the work. The owning repository is identified by domain.</p>
|
||||
<h3>Location</h3>
|
||||
<pre><repo-root>/workplans/<id>-<slug>.md</pre>
|
||||
<p>Examples:</p>
|
||||
<ul><li><code>the-custodian/workplans/CUST-WP-0005-dynamic-domains.md</code></li><li><code>railiance/workplans/RAIL-WP-0001-three-phoenix.md</code></li></ul>
|
||||
<h3>Frontmatter Schema</h3>
|
||||
<pre>---
|
||||
id: CUST-WP-0005 # human-readable workplan ID, unique per repo
|
||||
type: workplan
|
||||
title: "State Hub v0.5 — Dynamic Domains & Multi-Repo"
|
||||
domain: custodian # must match a registered domain slug
|
||||
status: active # active | completed | archived
|
||||
owner: custodian
|
||||
topic_slug: custodian # maps to a state-hub Topic slug
|
||||
created: "2026-02-28"
|
||||
updated: "2026-02-28"
|
||||
---</pre>
|
||||
<h3>Task Items</h3>
|
||||
<p>Tasks are embedded in the workplan file as headed sections. Each task section carries its own YAML block:</p>
|
||||
<pre>## P1.1 — Create `domains` table + Alembic migration
|
||||
</pre>
|
||||
<p>id: CUST-WP-0005-T001 status: todo priority: high</p>
|
||||
<pre>
|
||||
Task description prose here.</pre>
|
||||
<p>The state-hub parses these embedded task blocks during ingestion and upserts rows in the <code>tasks</code> table. The <code>id</code> field is the stable external key; the state-hub UUID is internal and opaque.</p>
|
||||
<h3>Decision Items</h3>
|
||||
<p>Decisions are separate files or embedded sections following the same pattern, using <code>type: decision</code> in frontmatter.</p>
|
||||
</section>
|
||||
<section id="rebuild-principle"><h2>Rebuild Principle</h2>
|
||||
<p>The rebuild sequence for a clean state-hub:</p>
|
||||
<ol><li><code>make migrate</code> — create schema</li><li><code>make seed-domains</code> — insert domain rows (domains.yaml in canon/)</li><li>For each registered repository: <code>make sync-workplans REPO=<slug></code> — parse workplan files and upsert workstreams, tasks, decisions</li><li><code>make sync-progress</code> — replay progress events from episodic memory logs</li></ol>
|
||||
<p>After step 4 the database must be functionally equivalent to the live state.</p>
|
||||
<section id="workplan-closure"><h2>Workplan closure</h2>
|
||||
<p>A workplan cannot be marked <code>finished</code> merely by changing its Hub row. Before closure, the responsible worker must:</p>
|
||||
<ol><li>review the source file and the projected view for unfinished tasks;</li><li>record completed and cancelled outcomes in the source task blocks using the canonical task lifecycle;</li><li>turn every actionable carry-forward item into a live work record with <code>origin: residual</code> and an <code>origin_ref</code>, or into another explicitly linked workplan;</li><li>set the source workplan to <code>finished</code>, update it, and commit the result;</li><li>publish the commit to Forge for the shared baseline and verify that the projection reconciles to that commit.</li></ol>
|
||||
<p>A local reconciliation before push may expose preliminary state, but it does not complete step 5.</p>
|
||||
<p>Automated stale-task cleanup must report file/projection drift. It may not silently cancel a file-backed task only in the database. An automated repair may change such a task only through the same governed repository mutation and commit path as any other file-backed change. Cleanup of hub-native records is governed by their own retention contract.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<h3>Immediate</h3>
|
||||
<ul><li>The v0.5 and v0.3 workplans created DB-first in this session are <strong>legacy records</strong> that violate this ADR. Remediation: write the corresponding workplan files, then mark the DB rows as <code>source: db-legacy</code> until a sync mechanism can reconcile them.</li></ul>
|
||||
<ul><li>The state-hub CLAUDE.md design-boundary note must be updated: the MCP bootstrap tools are permitted only as write-through tools (file + DB), never as DB-only tools.</li></ul>
|
||||
<h3>Medium Term</h3>
|
||||
<ul><li>A <code>make sync-workplans</code> command must be implemented as part of the managed-repos / contribution-tracking infrastructure (see v0.3 workplan).</li></ul>
|
||||
<ul><li>The <code>managed_repos</code> table is the prerequisite: the state-hub must know which repositories to scan.</li></ul>
|
||||
<ul><li>Workplan file format must be versioned and parsed by a dedicated loader (<code>state-hub/scripts/sync_workplans.py</code>).</li></ul>
|
||||
<h3>Long Term</h3>
|
||||
<ul><li>When the state-hub grows to cover multiple users or teams, this principle ensures that no coordination state can be lost by a database failure. Every repository is its own resilient shard of the coordination graph.</li></ul>
|
||||
<ul><li>This is the foundation for the "transgenerational" property: workplans in git survive database migrations, cloud provider changes, and system rebuilds.</li></ul>
|
||||
<h3>Positive</h3>
|
||||
<ul><li>Durable work remains inspectable, reviewable, and recoverable through Git.</li><li>The central view has an exact repository and commit provenance instead of reflecting whichever workstation synced most recently.</li><li>Reconciliation can be exercised per repository without destroying progress history or other hub-native evidence.</li><li>Multiple contributors share a published baseline while retaining an honest representation of preliminary work.</li><li>Service ownership is clearer: repositories own file-backed truth; the Hub owns runtime facts and projection services.</li></ul>
|
||||
<h3>Negative</h3>
|
||||
<ul><li>File-backed changes require a repository mutation and normally a push before they become shared baseline state.</li><li>Forge availability affects projection freshness, although it does not prevent local authoring.</li><li>Preliminary, stale, and retired projection states must be visible in APIs and user interfaces.</li><li>Git merge conflicts become the explicit conflict mechanism for simultaneous edits to the same authoritative artefact.</li><li>Tools that previously corrected only database state must be changed to emit a diagnostic or perform a governed repository update.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<p><strong>Database-first with export:</strong> Create in DB, export to files on demand. Rejected: export is easily skipped and files become secondary/stale.</p>
|
||||
<p><strong>Files-only, no database:</strong> Parse files on every query. Rejected: impractical at scale; the database is a necessary cache for cross-repo aggregation and real-time dashboard queries.</p>
|
||||
<p><strong>Hybrid with explicit sync flag:</strong> Mark some records as "db-authoritative" and others as "file-authoritative." Rejected: introduces ambiguity about which records matter; violates the "single source of truth" principle.</p>
|
||||
<section id="migration"><h2>Migration</h2>
|
||||
<p>No bulk rename, UUID rewrite, or historical file rewrite is required by this amendment. Existing identifiers and legacy terminology retain the grandfathering rules of ADR-007, ADR-011, and the terminology standard.</p>
|
||||
<p>Implementations must audit and retire DB-first creation, closure, and stale-task cleanup paths. Existing file-backed projection rows without a Forge source commit are migration state: they must be matched to a repository artefact or explicitly dispositioned before reset. Forge-derived reconciliation and preliminary overlays are implemented under ADR-012 rather than duplicated here.</p>
|
||||
<p>The <code>accepted-1</code> publication remains the immutable historical revision. This document is <code>accepted-2</code>.</p>
|
||||
</section>
|
||||
<section id="workplan-closure-protocol"><h2>Workplan Closure Protocol</h2>
|
||||
<p>When a workplan is about to be marked <code>finished</code>, the responsible agent MUST perform a closure review before writing the status change. This prevents the stale-task accumulation that this ADR was designed to make detectable.</p>
|
||||
<h3>Steps</h3>
|
||||
<ol><li><strong>Query all non-done tasks</strong> in the workplan via <code>GET /tasks/?workplan_id=<uuid></code> (legacy alias: <code>workstream_id</code>; filter for <code>todo</code>, <code>in_progress</code>, <code>blocked</code>).</li></ol>
|
||||
<ol><li><strong>Classify each task</strong> into one of three outcomes:</li></ol>
|
||||
<div class="scroll"><table><thead><tr><th>Outcome</th><th>Action</th></tr></thead><tbody><tr><td><strong>Done</strong> — work was completed, DB record just wasn't updated</td><td><code>PATCH /tasks/{id}/ {"status": "done"}</code></td></tr><tr><td><strong>Cancelled</strong> — dropped, superseded, or out of scope</td><td><code>PATCH /tasks/{id}/ {"status": "cancelled", "blocking_reason": "<why>"}</code></td></tr><tr><td><strong>Carry-forward</strong> — genuinely unfinished, belongs in the next run</td><td>Leave open; note in closure review; trigger new workplan</td></tr></tbody></table></div>
|
||||
<ol><li><strong>Append a <code>## Closure Review</code> section</strong> to the workplan file:</li></ol>
|
||||
<pre> ## Closure Review — YYYY-MM-DD
|
||||
|
||||
**Outcome:** All tasks completed / N tasks carried forward / N tasks dropped.
|
||||
|
||||
### Completed (DB updated)
|
||||
- TASK-ID — title
|
||||
|
||||
### Cancelled (dropped)
|
||||
| Task | Reason |
|
||||
|------|--------|
|
||||
| TASK-ID — title | Superseded by X |
|
||||
|
||||
### Carried forward
|
||||
| Task | Target workplan |
|
||||
|------|----------------|
|
||||
| TASK-ID — title | CUST-WP-XXXX |</pre>
|
||||
<ol><li><strong>If any tasks are carried forward</strong>: do not mark the workplan <code>finished</code> yet. Create the new workplan file (or amend an existing active one), then close the current workplan.</li></ol>
|
||||
<ol><li><strong>Update the workplan frontmatter</strong> <code>status: finished</code> and <code>updated:</code> date.</li></ol>
|
||||
<ol><li><strong>Mark the workplan <code>finished</code></strong> in the state hub via MCP or API (<code>update_workplan_status</code>).</li></ol>
|
||||
<h3>Daily Stale-Task Cleanup</h3>
|
||||
<p>As a safety net for cases where the closure review was skipped or incomplete, a cleanup script cancels any surviving open tasks in completed/archived workstreams:</p>
|
||||
<pre>cd ~/the-custodian/state-hub
|
||||
make cleanup-stale # run immediately
|
||||
# or add to cron:
|
||||
# 0 3 * * * cd ~/the-custodian/state-hub && make cleanup-stale</pre>
|
||||
<p>The script (<code>scripts/cleanup_stale_tasks.py</code>) emits a <code>cleanup</code> progress event recording which tasks were cancelled and in which workstreams. Tasks cancelled by the cleanup carry a <code>blocking_reason</code> noting they should be verified against the workplan file.</p>
|
||||
<p>The closure review is the primary mechanism; the cleanup is the fallback. If the cleanup regularly cancels tasks, it signals that closure reviews are being skipped — that is the process failure to address, not just the stale tasks.</p>
|
||||
<section id="alternatives-considered"><h2>Alternatives considered</h2>
|
||||
<p><strong>Database-first with export.</strong> Rejected. Export can be skipped, making the reviewable artefact secondary and stale.</p>
|
||||
<p><strong>Files only, with no projection database.</strong> Rejected. Cross-repository queries, runtime views, and append-only operational facts need indexed services.</p>
|
||||
<p><strong>Treat every Hub record as reconstructible from Git.</strong> Rejected. This either loses runtime truth during rebuild or creates artificial files whose only purpose is mirroring a database.</p>
|
||||
<p><strong>Let working copies push authoritative projection rows.</strong> Rejected by ADR-012. It makes the baseline depend on the last workstation to reconcile and cannot be audited against a shared commit.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>Custodian Constitution v0.1 §2 (Powers) — canon changes require review gate</li><li>ADR-000 (forthcoming) — overall Custodian architecture principles</li><li>State Hub v0.3 workplan — <code>sync_workplans.py</code> is a Phase 4 deliverable</li><li><code>canon/values/foundational_values_v0.1.md</code> — Local-first, Auditability, Reversibility</li></ul>
|
||||
</section><footer><span>CUST-ADR-001 · accepted-1 · accepted</span><span>the-custodian · canon/architecture/adr-001-workplans-as-repo-artefacts.md · 4039c9d1c08c92014ecc0a65dda63cc73ba187bb</span></footer></main></div></div></html>
|
||||
<ul><li>ADR-003 — materialized and derived state</li><li>ADR-005 — cross-repository work ownership</li><li>ADR-007 — workplan identity and repository worker topology</li><li>ADR-010 — Hub authority and local cache model</li><li>ADR-011 — namespace-aware federation and reconciliation limits</li><li>ADR-012 — Forge projection source and preliminary overlay</li><li><code>canon/standards/work-record-types_v0.1.md</code> — work-record kinds, lifecycle, residuals, and reconciliation</li><li><code>canon/standards/workplan-terminology-fleet_v0.1.md</code> — canonical terminology</li><li><code>canon/values/foundational_values_v0.1.md</code> — local-first operation, auditability, and reversibility</li></ul>
|
||||
</section><footer><span>CUST-ADR-001 · accepted-2 · accepted</span><span>the-custodian · canon/architecture/adr-001-workplans-as-repo-artefacts.md · 44500fc85cf29d8e9b2ee5c91994032ed3d04e5b</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d3c6f13d7aed9784b6f31c23d2b2777668493ba5">
|
||||
<meta name="policy-source-digest" content="183023ee57bae9c29e726fec5ee0361633fe3a2b182436a57869ae4b2a27af24">
|
||||
<title>Workplans and Work Items Are Repository Artefacts</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-001</span> <span class="stat">accepted · accepted-2</span> <span>the-custodian</span> <span>reviewed 2026-08-31</span><span>generated from canonical source — do not edit</span></div><h1>Workplans and Work Items Are Repository Artefacts</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-001-workplans-as-repo-artefacts.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#workplan-closure"><span class="n">·</span>Workplan closure</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#migration"><span class="n">·</span>Migration</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-02-28.</p>
|
||||
<p>Amended 2026-08-31 to distinguish file-backed work records from hub-native records, identify the Forge default branch as the central projection baseline, and align identity, lifecycle, reconciliation, and closure with ADR-007, ADR-010, ADR-011, ADR-012, and the work-record standards. The central decision is unchanged.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>During early State Hub development, workstreams and tasks were created directly in PostgreSQL through bootstrap APIs. This made a database the origin of durable work rather than a projection of repository-owned artefacts. The pattern was convenient, but it made work difficult to audit, review, recover, and carry between generations of tooling.</p>
|
||||
<p>The original decision correctly moved durable workplans and their work items into repositories. It overstated the consequence, however, by saying that the entire Hub database and every item that matters for coordination must be reconstructible from repository files. Later decisions established two different persistence classes:</p>
|
||||
<ul><li><strong>file-backed records</strong>, whose durable meaning originates in a repository;</li><li><strong>hub-native records</strong>, such as append-only progress and runtime facts, whose durable meaning originates in the Hub.</li></ul>
|
||||
<p>Later decisions also established that a central projection reads the pushed default branch in Forgejo. An arbitrary workstation checkout is a workspace, not the shared baseline. Unpushed local work may be represented only as an explicit preliminary overlay.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. File-backed work originates in the owning repository</h3>
|
||||
<p>Workplans and their file-backed work items <strong>MUST originate as repository artefacts in the repository that owns the work</strong>. The Hub indexes and projects those artefacts but is not their origin.</p>
|
||||
<p>This includes the workplan, embedded tasks, declared dependencies, residual handoffs, and durable governed decision artefacts when those are represented as files. No authoritative field of a file-backed record may exist solely in a Hub database.</p>
|
||||
<p>The owning repository is identified by repository identity, not merely by a domain or topic. Domains and topics classify work; they do not own its source.</p>
|
||||
<h3>2. Authority is explicit per persistence class</h3>
|
||||
<p>Every record contract must declare whether the record is file-backed or hub-native. The same record must not be writable as authoritative in both places.</p>
|
||||
<ul><li>For a <strong>file-backed record</strong>, the repository artefact is authoritative and the Hub row is derived state.</li><li>For a <strong>hub-native record</strong>, the Hub is authoritative. Progress events, messages, token events, run history, and similar runtime facts are not made fictional repository records merely to satisfy a rebuild slogan.</li></ul>
|
||||
<p>The word <code>decision</code> is used by more than one subsystem. Architecture decisions and other governed decision artefacts remain files. A runtime decision event may be hub-native only where its schema says so explicitly; it does not replace the governed artefact.</p>
|
||||
<p>This replaces the original blanket rejection of a hybrid architecture. The architecture is hybrid <strong>by declared record class</strong>, never ambiguous within one record.</p>
|
||||
<h3>3. Forge is the central projection baseline</h3>
|
||||
<p>For the shared central view, the source is the pushed default-branch state held by Forgejo, as decided by ADR-012. Every projected record must be attributable to its source repository, path, and commit.</p>
|
||||
<p>A working copy remains the authoring workspace. Unpushed work may appear in the Hub only as an attributed preliminary overlay. It must not silently replace or be presented as the Forge-derived baseline. When its commit reaches Forge, the baseline absorbs it and the overlay retires.</p>
|
||||
<h3>4. Mutations of file-backed state are file-first</h3>
|
||||
<p>Creating or changing a file-backed work record means changing its repository artefact first. A convenience tool or API is conformant only when it performs a governed repository mutation and leaves a reviewable file and commit. Writing only the projected database row is not a durable update.</p>
|
||||
<p>Direct Hub APIs remain valid for hub-native records. They may also provide diagnostics or propose repository patches, but a successful database PATCH is not evidence that a file-backed status changed.</p>
|
||||
<h3>5. The rebuild guarantee applies to the projection, not the whole database</h3>
|
||||
<p>Given a repository and a specific Forge commit, the system must be able to reconcile that repository's file-derived projection so that it is equivalent to the records derived from that commit. Reconciliation must be idempotent, verifiable, and scoped per repository; a fleet operation is iteration over the same per-repository operation.</p>
|
||||
<p>Reconciliation retires file-derived records that no longer derive. It does not delete hub-native history attached to them. It must refuse and report a repository whose records exist only in the projection until those records have an explicit disposition. A Forge rebuild does not reconstruct preliminary overlays and must disclose their retirement before proceeding.</p>
|
||||
<p>Therefore the <strong>file-derived projection is disposable</strong>. The database as a whole is not disposable when it also contains hub-native facts.</p>
|
||||
<h3>6. Identity and lifecycle are delegated contracts</h3>
|
||||
<p>This ADR does not define a second identity or lifecycle schema.</p>
|
||||
<ul><li>Workplan and task identity follow ADR-007 as amended by ADR-011: global identity is namespace-aware, and derivable Hub identifiers are governed by that contract. Existing historical identifiers are grandfathered according to its migration rules.</li><li>Work-record kinds, locations, lifecycle values, and residual handling follow <code>canon/standards/work-record-types_v0.1.md</code> and <code>canon/standards/workplan-terminology-fleet_v0.1.md</code>.</li></ul>
|
||||
<p>New normative text uses <code>workplan</code> and <code>work record</code>. <code>workstream</code> remains only as a metered compatibility term for legacy database and API surfaces.</p>
|
||||
<h3>7. Reconciliation belongs at the repository boundary</h3>
|
||||
<p>Repo Manager owns discovery, parsing, identity checks, and reconciliation for file-backed records. State Hub owns the projection and hub-native records. The implementation may distribute fetch and parse work, but it must preserve that authority boundary.</p>
|
||||
<p>Legacy <code>create_workstream</code>, <code>create_task</code>, workstation-driven <code>sync-workplans</code>, and similarly named commands are not normative interfaces. They are conformant only if their current implementation satisfies decisions 3 and 4; otherwise they are transitional or retired.</p>
|
||||
</section>
|
||||
<section id="workplan-closure"><h2>Workplan closure</h2>
|
||||
<p>A workplan cannot be marked <code>finished</code> merely by changing its Hub row. Before closure, the responsible worker must:</p>
|
||||
<ol><li>review the source file and the projected view for unfinished tasks;</li><li>record completed and cancelled outcomes in the source task blocks using the canonical task lifecycle;</li><li>turn every actionable carry-forward item into a live work record with <code>origin: residual</code> and an <code>origin_ref</code>, or into another explicitly linked workplan;</li><li>set the source workplan to <code>finished</code>, update it, and commit the result;</li><li>publish the commit to Forge for the shared baseline and verify that the projection reconciles to that commit.</li></ol>
|
||||
<p>A local reconciliation before push may expose preliminary state, but it does not complete step 5.</p>
|
||||
<p>Automated stale-task cleanup must report file/projection drift. It may not silently cancel a file-backed task only in the database. An automated repair may change such a task only through the same governed repository mutation and commit path as any other file-backed change. Cleanup of hub-native records is governed by their own retention contract.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<h3>Positive</h3>
|
||||
<ul><li>Durable work remains inspectable, reviewable, and recoverable through Git.</li><li>The central view has an exact repository and commit provenance instead of reflecting whichever workstation synced most recently.</li><li>Reconciliation can be exercised per repository without destroying progress history or other hub-native evidence.</li><li>Multiple contributors share a published baseline while retaining an honest representation of preliminary work.</li><li>Service ownership is clearer: repositories own file-backed truth; the Hub owns runtime facts and projection services.</li></ul>
|
||||
<h3>Negative</h3>
|
||||
<ul><li>File-backed changes require a repository mutation and normally a push before they become shared baseline state.</li><li>Forge availability affects projection freshness, although it does not prevent local authoring.</li><li>Preliminary, stale, and retired projection states must be visible in APIs and user interfaces.</li><li>Git merge conflicts become the explicit conflict mechanism for simultaneous edits to the same authoritative artefact.</li><li>Tools that previously corrected only database state must be changed to emit a diagnostic or perform a governed repository update.</li></ul>
|
||||
</section>
|
||||
<section id="migration"><h2>Migration</h2>
|
||||
<p>No bulk rename, UUID rewrite, or historical file rewrite is required by this amendment. Existing identifiers and legacy terminology retain the grandfathering rules of ADR-007, ADR-011, and the terminology standard.</p>
|
||||
<p>Implementations must audit and retire DB-first creation, closure, and stale-task cleanup paths. Existing file-backed projection rows without a Forge source commit are migration state: they must be matched to a repository artefact or explicitly dispositioned before reset. Forge-derived reconciliation and preliminary overlays are implemented under ADR-012 rather than duplicated here.</p>
|
||||
<p>The <code>accepted-1</code> publication remains the immutable historical revision. This document is <code>accepted-2</code>.</p>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives considered</h2>
|
||||
<p><strong>Database-first with export.</strong> Rejected. Export can be skipped, making the reviewable artefact secondary and stale.</p>
|
||||
<p><strong>Files only, with no projection database.</strong> Rejected. Cross-repository queries, runtime views, and append-only operational facts need indexed services.</p>
|
||||
<p><strong>Treat every Hub record as reconstructible from Git.</strong> Rejected. This either loses runtime truth during rebuild or creates artificial files whose only purpose is mirroring a database.</p>
|
||||
<p><strong>Let working copies push authoritative projection rows.</strong> Rejected by ADR-012. It makes the baseline depend on the last workstation to reconcile and cannot be audited against a shared commit.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li>ADR-003 — materialized and derived state</li><li>ADR-005 — cross-repository work ownership</li><li>ADR-007 — workplan identity and repository worker topology</li><li>ADR-010 — Hub authority and local cache model</li><li>ADR-011 — namespace-aware federation and reconciliation limits</li><li>ADR-012 — Forge projection source and preliminary overlay</li><li><code>canon/standards/work-record-types_v0.1.md</code> — work-record kinds, lifecycle, residuals, and reconciliation</li><li><code>canon/standards/workplan-terminology-fleet_v0.1.md</code> — canonical terminology</li><li><code>canon/values/foundational_values_v0.1.md</code> — local-first operation, auditability, and reversibility</li></ul>
|
||||
</section><footer><span>CUST-ADR-001 · accepted-2 · accepted</span><span>the-custodian · canon/architecture/adr-001-workplans-as-repo-artefacts.md · d3c6f13d7aed9784b6f31c23d2b2777668493ba5</span></footer></main></div></div></html>
|
||||
227
build/adr/netkingdom-iam-profile-governance/v1/index.html
Normal file
227
build/adr/netkingdom-iam-profile-governance/v1/index.html
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="b5c7fd1e78026063b4a2ca4017202512d4f94ab5e12dc8498a34d04d3a48c7a9">
|
||||
<title>NetKingdom IAM Profile Ownership And Version Governance</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0011</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom IAM Profile Ownership And Version Governance</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0011-iam-profile-ownership-and-version-governance.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#versioning"><span class="n">·</span>Versioning</a></li><li><a href="#breaking-change-governance"><span class="n">·</span>Breaking-Change Governance</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-22 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The IAM Profile is the identity contract that applications, flex-auth, key-cape, Keycloak, and bootstrap identity tooling all target. It defines the OIDC discovery, flow, token, claim, assurance, tenant, and conformance requirements that make lightweight and expanded identity modes interchangeable at the application boundary.</p>
|
||||
<p>A draft IAM Profile v0.1 existed in the-custodian canon with an all-hubs scope. That draft captured useful material: OIDC discovery, Authorization Code + PKCE, service-account tokens, required claims, token lifecycle, emergency access, and local-development behavior. However, NetKingdom now owns the platform identity domain. SCOPE.md names the NetKingdom IAM Profile as an in-scope, versioned standard, and ADR-0006 requires key-cape and Keycloak to be implementations of the profile rather than the canonical source of authorization semantics.</p>
|
||||
<p>The v0.1 draft also used hub-specific scope and role vocabulary. That made sense for the Custodian hub landscape, but the core NetKingdom profile must be platform-neutral so it can serve tenant, service, application, and agent use cases without encoding one downstream system's scope names.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>NetKingdom is the canonical owner of the IAM Profile.</p>
|
||||
<p>The profile is versioned under <code>canon/standards/</code> in this repository. The first canonical NetKingdom version is <code>canon/standards/iam-profile_v0.2.md</code>.</p>
|
||||
<p>The relationship to the earlier the-custodian draft is:</p>
|
||||
<ul><li>the-custodian IAM Profile v0.1 is superseded as a core/platform standard;</li><li>NetKingdom owns the provider-neutral core profile;</li><li>downstream systems may define hub-, tenant-, or application-specific scopes and roles as extensions, but those extensions must map back to the core identity and authorization input contract;</li><li>key-cape lightweight mode and Keycloak expanded mode are interchangeable implementations of the same profile;</li><li>flex-auth consumes the profile as normative identity input and must not re-derive identity facts from provider-specific state.</li></ul>
|
||||
</section>
|
||||
<section id="versioning"><h2>Versioning</h2>
|
||||
<p>The IAM Profile uses explicit document versions:</p>
|
||||
<ul><li>Patch/editorial changes clarify wording, examples, or non-normative guidance without changing the token contract.</li><li>Minor versions add optional claims, optional flows, or additional conformance checks that existing implementations can pass unchanged.</li><li>Major or breaking versions change required claims, claim meanings, validation rules, flow requirements, assurance semantics, tenant semantics, or token acceptance rules.</li></ul>
|
||||
<p>Every versioned profile file remains immutable enough for downstream references to cite. New versions are added as new files rather than rewriting historical versions in place, except for clearly editorial fixes that do not affect semantics.</p>
|
||||
</section>
|
||||
<section id="breaking-change-governance"><h2>Breaking-Change Governance</h2>
|
||||
<p>A breaking profile change requires:</p>
|
||||
<ol><li>a new ADR or ADR refinement that explains the change and migration path;</li><li>a new versioned profile document;</li><li>an update to the executable conformance suite;</li><li>a coexistence window that lets at least one previous supported profile version and the new version be accepted where practical;</li><li>notification in workplans or interface docs for known consumers, especially key-cape, Keycloak/expanded-mode work, flex-auth, and application integration docs.</li></ol>
|
||||
<p>Breaking changes include:</p>
|
||||
<ul><li>removing or renaming a required claim;</li><li>changing the meaning, type, or allowed values of required claims such as <code>tenant</code>, <code>principal_type</code>, <code>roles</code>, <code>groups</code>, <code>scope</code>/<code>scp</code>, or <code>assurance</code>;</li><li>changing accepted issuer, audience, or signing validation rules;</li><li>weakening PKCE, MFA/assurance, local-development rejection, or emergency-access requirements;</li><li>moving authorization decisions into an identity provider instead of flex-auth.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>canon/standards/iam-profile_v0.2.md</code> is the canonical profile.</li><li>the-custodian's v0.1 draft should carry a relocation/deprecation note pointing to this repository.</li><li>Hub-specific scopes such as <code>hub:*</code>, <code>ops:*</code>, and <code>fin:*</code> are downstream extensions, not core profile vocabulary.</li><li>key-cape and Keycloak must emit or normalize to the same claim contract before applications and flex-auth consume tokens.</li><li>The conformance suite in <code>tools/iam-profile-conformance/</code> is the executable contract for implementations.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Keep The Custodian Draft As Canonical</h3>
|
||||
<p>The draft is useful, but keeping ownership there would conflict with NetKingdom's repository scope and with ADR-0006's responsibility split. It would also leave the profile coupled to Custodian hub vocabulary.</p>
|
||||
<h3>Make Keycloak The Reference Provider</h3>
|
||||
<p>Keycloak is the expanded-mode implementation and remains important for enterprise federation. Making it the reference provider would make lightweight mode, local bootstrap, and future identity adapters secondary to one implementation. The accepted model keeps providers interchangeable behind the profile.</p>
|
||||
<h3>Put Scope And Role Vocabulary In The Core Profile</h3>
|
||||
<p>A shared vocabulary is useful, but core identity must stay stable across applications and tenants. Downstream systems can define extension scopes and roles as long as they map to the core claim shapes and flex-auth decision inputs.</p>
|
||||
</section><footer><span>NK-ADR-0011 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0011-iam-profile-ownership-and-version-governance.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="b5c7fd1e78026063b4a2ca4017202512d4f94ab5e12dc8498a34d04d3a48c7a9">
|
||||
<title>NetKingdom IAM Profile Ownership And Version Governance</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0011</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom IAM Profile Ownership And Version Governance</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0011-iam-profile-ownership-and-version-governance.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#versioning"><span class="n">·</span>Versioning</a></li><li><a href="#breaking-change-governance"><span class="n">·</span>Breaking-Change Governance</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-22 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The IAM Profile is the identity contract that applications, flex-auth, key-cape, Keycloak, and bootstrap identity tooling all target. It defines the OIDC discovery, flow, token, claim, assurance, tenant, and conformance requirements that make lightweight and expanded identity modes interchangeable at the application boundary.</p>
|
||||
<p>A draft IAM Profile v0.1 existed in the-custodian canon with an all-hubs scope. That draft captured useful material: OIDC discovery, Authorization Code + PKCE, service-account tokens, required claims, token lifecycle, emergency access, and local-development behavior. However, NetKingdom now owns the platform identity domain. SCOPE.md names the NetKingdom IAM Profile as an in-scope, versioned standard, and ADR-0006 requires key-cape and Keycloak to be implementations of the profile rather than the canonical source of authorization semantics.</p>
|
||||
<p>The v0.1 draft also used hub-specific scope and role vocabulary. That made sense for the Custodian hub landscape, but the core NetKingdom profile must be platform-neutral so it can serve tenant, service, application, and agent use cases without encoding one downstream system's scope names.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>NetKingdom is the canonical owner of the IAM Profile.</p>
|
||||
<p>The profile is versioned under <code>canon/standards/</code> in this repository. The first canonical NetKingdom version is <code>canon/standards/iam-profile_v0.2.md</code>.</p>
|
||||
<p>The relationship to the earlier the-custodian draft is:</p>
|
||||
<ul><li>the-custodian IAM Profile v0.1 is superseded as a core/platform standard;</li><li>NetKingdom owns the provider-neutral core profile;</li><li>downstream systems may define hub-, tenant-, or application-specific scopes and roles as extensions, but those extensions must map back to the core identity and authorization input contract;</li><li>key-cape lightweight mode and Keycloak expanded mode are interchangeable implementations of the same profile;</li><li>flex-auth consumes the profile as normative identity input and must not re-derive identity facts from provider-specific state.</li></ul>
|
||||
</section>
|
||||
<section id="versioning"><h2>Versioning</h2>
|
||||
<p>The IAM Profile uses explicit document versions:</p>
|
||||
<ul><li>Patch/editorial changes clarify wording, examples, or non-normative guidance without changing the token contract.</li><li>Minor versions add optional claims, optional flows, or additional conformance checks that existing implementations can pass unchanged.</li><li>Major or breaking versions change required claims, claim meanings, validation rules, flow requirements, assurance semantics, tenant semantics, or token acceptance rules.</li></ul>
|
||||
<p>Every versioned profile file remains immutable enough for downstream references to cite. New versions are added as new files rather than rewriting historical versions in place, except for clearly editorial fixes that do not affect semantics.</p>
|
||||
</section>
|
||||
<section id="breaking-change-governance"><h2>Breaking-Change Governance</h2>
|
||||
<p>A breaking profile change requires:</p>
|
||||
<ol><li>a new ADR or ADR refinement that explains the change and migration path;</li><li>a new versioned profile document;</li><li>an update to the executable conformance suite;</li><li>a coexistence window that lets at least one previous supported profile version and the new version be accepted where practical;</li><li>notification in workplans or interface docs for known consumers, especially key-cape, Keycloak/expanded-mode work, flex-auth, and application integration docs.</li></ol>
|
||||
<p>Breaking changes include:</p>
|
||||
<ul><li>removing or renaming a required claim;</li><li>changing the meaning, type, or allowed values of required claims such as <code>tenant</code>, <code>principal_type</code>, <code>roles</code>, <code>groups</code>, <code>scope</code>/<code>scp</code>, or <code>assurance</code>;</li><li>changing accepted issuer, audience, or signing validation rules;</li><li>weakening PKCE, MFA/assurance, local-development rejection, or emergency-access requirements;</li><li>moving authorization decisions into an identity provider instead of flex-auth.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>canon/standards/iam-profile_v0.2.md</code> is the canonical profile.</li><li>the-custodian's v0.1 draft should carry a relocation/deprecation note pointing to this repository.</li><li>Hub-specific scopes such as <code>hub:*</code>, <code>ops:*</code>, and <code>fin:*</code> are downstream extensions, not core profile vocabulary.</li><li>key-cape and Keycloak must emit or normalize to the same claim contract before applications and flex-auth consume tokens.</li><li>The conformance suite in <code>tools/iam-profile-conformance/</code> is the executable contract for implementations.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Keep The Custodian Draft As Canonical</h3>
|
||||
<p>The draft is useful, but keeping ownership there would conflict with NetKingdom's repository scope and with ADR-0006's responsibility split. It would also leave the profile coupled to Custodian hub vocabulary.</p>
|
||||
<h3>Make Keycloak The Reference Provider</h3>
|
||||
<p>Keycloak is the expanded-mode implementation and remains important for enterprise federation. Making it the reference provider would make lightweight mode, local bootstrap, and future identity adapters secondary to one implementation. The accepted model keeps providers interchangeable behind the profile.</p>
|
||||
<h3>Put Scope And Role Vocabulary In The Core Profile</h3>
|
||||
<p>A shared vocabulary is useful, but core identity must stay stable across applications and tenants. Downstream systems can define extension scopes and roles as long as they map to the core claim shapes and flex-auth decision inputs.</p>
|
||||
</section><footer><span>NK-ADR-0011 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0011-iam-profile-ownership-and-version-governance.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="f47276f4953f62b783397ee7fb1d3693da060103247e425a9a5b40d019fb4272">
|
||||
<title>Object Storage STS Credential Vending Boundary</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0008</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Object Storage STS Credential Vending Boundary</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0008-object-storage-sts-credential-vending.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-18 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>NetKingdom needs a canonical pattern for issuing short-lived object-storage credentials to platform and tenant workloads. The first known consumer is <code>artifact-store</code>, but the pattern must work for future S3-compatible consumers without making each application repo own identity, authorization, root object-store credentials, or backend-specific STS differences.</p>
|
||||
<p>The backend landscape is not uniform. AWS S3, Ceph RGW, and MinIO/AIStor can use web-identity STS-style flows. Cloudflare R2 exposes temporary credentials through a provider API or local signing with parent access material. OpenBao is now part of the Railiance platform stack as runtime secret authority, but it is not an identity provider or authorization policy engine.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>NetKingdom will define a provider-neutral credential-vending interface backed by provider-native temporary credential mechanisms where possible.</p>
|
||||
<p>The trust path is:</p>
|
||||
<ol><li>IAM Profile token proves the actor or workload.</li><li>flex-auth decides whether the actor may receive credentials for the requested protected system, tenant, bucket, prefix, action set, TTL, and assurance level.</li><li>The credential-vending service exchanges the approved request with the backend-specific temporary credential mechanism.</li><li>OpenBao stores parent credentials, broker configuration, lease metadata, and audit evidence where useful, but it does not replace flex-auth authorization.</li><li>Consumers receive normalized temporary credentials containing access key id, secret access key, session token, and expiration.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>artifact-store</code> needs temporary credential support, especially <code>AWS_SESSION_TOKEN</code> and refresh behavior, before it can fully consume the production vending pattern.</li><li>Backend-specific differences are isolated in the vending service, not leaked into application policy.</li><li>OpenBao remains runtime secret infrastructure and audit support; it does not become the object-storage policy source.</li><li>Provider-native STS is preferred when available because it gives the storage backend direct lease/expiration semantics.</li><li>Cloudflare R2 requires a broker path that protects parent access material, most likely through OpenBao custody.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Give Applications Long-Lived Access Keys</h3>
|
||||
<p>This is simple but leaves applications holding durable credentials and pushes policy into ad hoc bucket configuration. It is acceptable only as a transitional bridge with scoped credentials and explicit rotation.</p>
|
||||
<h3>Put Object-Storage Policy In Keycloak Or key-cape</h3>
|
||||
<p>Identity providers can assert who the actor is and coarse groups or roles, but they should not become the canonical source of bucket, prefix, action, TTL, and explanation semantics.</p>
|
||||
<h3>Use OpenBao As The Credential Vending Policy Engine</h3>
|
||||
<p>OpenBao is valuable for secret custody, broker configuration, leases, and audit records. Making it the policy decision point would duplicate flex-auth, blur the platform/tenant boundary, and make authorization semantics backend-specific.</p>
|
||||
<h3>Require One Backend Everywhere</h3>
|
||||
<p>A single backend would simplify implementation but does not match the platform direction. Railiance and NetKingdom need a stable security interface across AWS, self-hosted S3-compatible stores, and Cloudflare R2-like APIs.</p>
|
||||
</section><footer><span>NK-ADR-0008 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0008-object-storage-sts-credential-vending.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="f47276f4953f62b783397ee7fb1d3693da060103247e425a9a5b40d019fb4272">
|
||||
<title>Object Storage STS Credential Vending Boundary</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0008</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Object Storage STS Credential Vending Boundary</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0008-object-storage-sts-credential-vending.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-18 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>NetKingdom needs a canonical pattern for issuing short-lived object-storage credentials to platform and tenant workloads. The first known consumer is <code>artifact-store</code>, but the pattern must work for future S3-compatible consumers without making each application repo own identity, authorization, root object-store credentials, or backend-specific STS differences.</p>
|
||||
<p>The backend landscape is not uniform. AWS S3, Ceph RGW, and MinIO/AIStor can use web-identity STS-style flows. Cloudflare R2 exposes temporary credentials through a provider API or local signing with parent access material. OpenBao is now part of the Railiance platform stack as runtime secret authority, but it is not an identity provider or authorization policy engine.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>NetKingdom will define a provider-neutral credential-vending interface backed by provider-native temporary credential mechanisms where possible.</p>
|
||||
<p>The trust path is:</p>
|
||||
<ol><li>IAM Profile token proves the actor or workload.</li><li>flex-auth decides whether the actor may receive credentials for the requested protected system, tenant, bucket, prefix, action set, TTL, and assurance level.</li><li>The credential-vending service exchanges the approved request with the backend-specific temporary credential mechanism.</li><li>OpenBao stores parent credentials, broker configuration, lease metadata, and audit evidence where useful, but it does not replace flex-auth authorization.</li><li>Consumers receive normalized temporary credentials containing access key id, secret access key, session token, and expiration.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>artifact-store</code> needs temporary credential support, especially <code>AWS_SESSION_TOKEN</code> and refresh behavior, before it can fully consume the production vending pattern.</li><li>Backend-specific differences are isolated in the vending service, not leaked into application policy.</li><li>OpenBao remains runtime secret infrastructure and audit support; it does not become the object-storage policy source.</li><li>Provider-native STS is preferred when available because it gives the storage backend direct lease/expiration semantics.</li><li>Cloudflare R2 requires a broker path that protects parent access material, most likely through OpenBao custody.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Give Applications Long-Lived Access Keys</h3>
|
||||
<p>This is simple but leaves applications holding durable credentials and pushes policy into ad hoc bucket configuration. It is acceptable only as a transitional bridge with scoped credentials and explicit rotation.</p>
|
||||
<h3>Put Object-Storage Policy In Keycloak Or key-cape</h3>
|
||||
<p>Identity providers can assert who the actor is and coarse groups or roles, but they should not become the canonical source of bucket, prefix, action, TTL, and explanation semantics.</p>
|
||||
<h3>Use OpenBao As The Credential Vending Policy Engine</h3>
|
||||
<p>OpenBao is valuable for secret custody, broker configuration, leases, and audit records. Making it the policy decision point would duplicate flex-auth, blur the platform/tenant boundary, and make authorization semantics backend-specific.</p>
|
||||
<h3>Require One Backend Everywhere</h3>
|
||||
<p>A single backend would simplify implementation but does not match the platform direction. Railiance and NetKingdom need a stable security interface across AWS, self-hosted S3-compatible stores, and Cloudflare R2-like APIs.</p>
|
||||
</section><footer><span>NK-ADR-0008 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0008-object-storage-sts-credential-vending.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="b7c4f6a13f2f5add08bd03cb39c4f18ca25b202747dde883a309d1b3eaa1f571">
|
||||
<title>Orchestration vs Dependency, and Self-Coherent Intent</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0010</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Orchestration vs Dependency, and Self-Coherent Intent</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0010-orchestration-vs-dependency-self-coherent-intent.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted (repo classification subject to ongoing refinement) <strong>Date:</strong> 2026-05-21 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>While aligning the ecosystem's <code>INTENT.md</code> files, two relationships that had been blurred turned out to be fundamentally different, and a content principle for intent emerged. Both are foundational enough that future interface and boundary refinements should be measured against them.</p>
|
||||
<p>NetKingdom performs meta-orchestration (ADR-0007): it selects, parametrizes, and assigns responsibility across an IT landscape. But "things NetKingdom meta-orchestrates" is not the same as "things NetKingdom depends on," and the two had been conflated.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>Principle 1 — Orchestration is not dependency</h3>
|
||||
<p>NetKingdom relates to other repositories in two distinct ways:</p>
|
||||
<ul><li><strong>Orchestrated</strong> — the repo provides a service that <strong>holds resources NetKingdom must manage</strong>: users, roles, scopes, policies, credentials, infrastructure resources, and the like. NetKingdom composes, parametrizes, and holds responsibility for those resources.</li><li><strong>Dependency</strong> — NetKingdom <strong>uses the repo as a tool</strong> to provide its own interface, without managing resources the tool holds.</li></ul>
|
||||
<p><strong>The defining question:</strong> <em>does the repo provide a service holding resources that NetKingdom needs to orchestrate?</em></p>
|
||||
<ul><li>Yes → <strong>orchestrated</strong>.</li><li>No, it is merely used → <strong>dependency</strong>.</li></ul>
|
||||
<p>Worked examples:</p>
|
||||
<ul><li><code>railiance-fabric</code> is a tool NetKingdom uses to provide an interface; it holds no NetKingdom-managed resources → <strong>dependency</strong>.</li><li><code>railiance-infra</code>, <code>railiance-cluster</code>, <code>railiance-platform</code> define and hold resources → <strong>orchestrated</strong>.</li><li>An IAM directory (users, groups) or a policy store (roles, scopes, policies) holds exactly the resource kinds in the criterion → <strong>orchestrated</strong>.</li></ul>
|
||||
<p>This classification is applied now (see the responsibility map) and will be refined as interfaces and boundaries mature. Borderline cases are expected.</p>
|
||||
<h3>Principle 2 — Intent is self-coherent</h3>
|
||||
<p>Every repository's <code>INTENT.md</code> describes <strong>that repository's own</strong> purpose and direction, abstractly and stably. Therefore:</p>
|
||||
<ul><li>It must <strong>not</strong> define itself in terms of NetKingdom.</li><li>It must <strong>not</strong> reference the intent of sister projects.</li><li>It must <strong>not</strong> even encode dependencies — dependencies are more concrete and less stable than intent should be.</li></ul>
|
||||
<p>Intent is the most abstract, most stable layer. Relationships — orchestration, dependency, interfaces, boundaries — are recorded <strong>outside</strong> intent: in NetKingdom's responsibility map, architecture docs, ADRs, and interface contracts. This keeps every repo's intent free of external reference points, so it stays stable while the interfaces and boundaries between repos are refined over time.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>The earlier idea of adding a "place in the NetKingdom-orchestrated landscape" block to downstream <code>INTENT.md</code> files is <strong>rejected</strong>. It would violate Principle 2.</li><li>Cross-repo <code>INTENT.md</code> work becomes: ensure each orchestrated repo has a <strong>self-coherent</strong> intent — author one where missing, and remove external references (to NetKingdom or sister projects) where present.</li><li>The orchestration/dependency relationship and the per-repo <strong>responsibility map</strong> live in net-kingdom, not in the downstream repos.</li><li>A responsibility-map artifact in net-kingdom enumerates, per orchestrated repo, which resources NetKingdom manages: <code>docs/responsibility-map.md</code>.</li><li>ADR-0007's meta-orchestration layer is unchanged; this ADR clarifies <em>what</em> NetKingdom orchestrates versus merely uses.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Treat every related repo uniformly</h3>
|
||||
<p>Simpler, but it conflates "manages the resources this service holds" with "uses this tool," which produces an incoherent responsibility map and tempts downstream repos to encode NetKingdom into their intent.</p>
|
||||
<h3>Record relationships inside each repo's intent</h3>
|
||||
<p>Convenient for a reader of a single repo, but it couples intents to each other and to NetKingdom, making the most-stable layer the least stable. Relationships belong in interface contracts and the responsibility map.</p>
|
||||
</section><footer><span>NK-ADR-0010 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0010-orchestration-vs-dependency-self-coherent-intent.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="b7c4f6a13f2f5add08bd03cb39c4f18ca25b202747dde883a309d1b3eaa1f571">
|
||||
<title>Orchestration vs Dependency, and Self-Coherent Intent</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0010</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Orchestration vs Dependency, and Self-Coherent Intent</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0010-orchestration-vs-dependency-self-coherent-intent.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted (repo classification subject to ongoing refinement) <strong>Date:</strong> 2026-05-21 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>While aligning the ecosystem's <code>INTENT.md</code> files, two relationships that had been blurred turned out to be fundamentally different, and a content principle for intent emerged. Both are foundational enough that future interface and boundary refinements should be measured against them.</p>
|
||||
<p>NetKingdom performs meta-orchestration (ADR-0007): it selects, parametrizes, and assigns responsibility across an IT landscape. But "things NetKingdom meta-orchestrates" is not the same as "things NetKingdom depends on," and the two had been conflated.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>Principle 1 — Orchestration is not dependency</h3>
|
||||
<p>NetKingdom relates to other repositories in two distinct ways:</p>
|
||||
<ul><li><strong>Orchestrated</strong> — the repo provides a service that <strong>holds resources NetKingdom must manage</strong>: users, roles, scopes, policies, credentials, infrastructure resources, and the like. NetKingdom composes, parametrizes, and holds responsibility for those resources.</li><li><strong>Dependency</strong> — NetKingdom <strong>uses the repo as a tool</strong> to provide its own interface, without managing resources the tool holds.</li></ul>
|
||||
<p><strong>The defining question:</strong> <em>does the repo provide a service holding resources that NetKingdom needs to orchestrate?</em></p>
|
||||
<ul><li>Yes → <strong>orchestrated</strong>.</li><li>No, it is merely used → <strong>dependency</strong>.</li></ul>
|
||||
<p>Worked examples:</p>
|
||||
<ul><li><code>railiance-fabric</code> is a tool NetKingdom uses to provide an interface; it holds no NetKingdom-managed resources → <strong>dependency</strong>.</li><li><code>railiance-infra</code>, <code>railiance-cluster</code>, <code>railiance-platform</code> define and hold resources → <strong>orchestrated</strong>.</li><li>An IAM directory (users, groups) or a policy store (roles, scopes, policies) holds exactly the resource kinds in the criterion → <strong>orchestrated</strong>.</li></ul>
|
||||
<p>This classification is applied now (see the responsibility map) and will be refined as interfaces and boundaries mature. Borderline cases are expected.</p>
|
||||
<h3>Principle 2 — Intent is self-coherent</h3>
|
||||
<p>Every repository's <code>INTENT.md</code> describes <strong>that repository's own</strong> purpose and direction, abstractly and stably. Therefore:</p>
|
||||
<ul><li>It must <strong>not</strong> define itself in terms of NetKingdom.</li><li>It must <strong>not</strong> reference the intent of sister projects.</li><li>It must <strong>not</strong> even encode dependencies — dependencies are more concrete and less stable than intent should be.</li></ul>
|
||||
<p>Intent is the most abstract, most stable layer. Relationships — orchestration, dependency, interfaces, boundaries — are recorded <strong>outside</strong> intent: in NetKingdom's responsibility map, architecture docs, ADRs, and interface contracts. This keeps every repo's intent free of external reference points, so it stays stable while the interfaces and boundaries between repos are refined over time.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>The earlier idea of adding a "place in the NetKingdom-orchestrated landscape" block to downstream <code>INTENT.md</code> files is <strong>rejected</strong>. It would violate Principle 2.</li><li>Cross-repo <code>INTENT.md</code> work becomes: ensure each orchestrated repo has a <strong>self-coherent</strong> intent — author one where missing, and remove external references (to NetKingdom or sister projects) where present.</li><li>The orchestration/dependency relationship and the per-repo <strong>responsibility map</strong> live in net-kingdom, not in the downstream repos.</li><li>A responsibility-map artifact in net-kingdom enumerates, per orchestrated repo, which resources NetKingdom manages: <code>docs/responsibility-map.md</code>.</li><li>ADR-0007's meta-orchestration layer is unchanged; this ADR clarifies <em>what</em> NetKingdom orchestrates versus merely uses.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Treat every related repo uniformly</h3>
|
||||
<p>Simpler, but it conflates "manages the resources this service holds" with "uses this tool," which produces an incoherent responsibility map and tempts downstream repos to encode NetKingdom into their intent.</p>
|
||||
<h3>Record relationships inside each repo's intent</h3>
|
||||
<p>Convenient for a reader of a single repo, but it couples intents to each other and to NetKingdom, making the most-stable layer the least stable. Relationships belong in interface contracts and the responsibility map.</p>
|
||||
</section><footer><span>NK-ADR-0010 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0010-orchestration-vs-dependency-self-coherent-intent.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
227
build/adr/netkingdom-playbook-capability-ownership/v1/index.html
Normal file
227
build/adr/netkingdom-playbook-capability-ownership/v1/index.html
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="e269fabfc376f97f2a03ea66e34059d54016a445a7f30b351a6774b65c96c2ee">
|
||||
<title>Playbook Capability Contract Ownership</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0012</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Playbook Capability Contract Ownership</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0012-playbook-capability-contract-ownership.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#versioning"><span class="n">·</span>Versioning</a></li><li><a href="#breaking-change-governance"><span class="n">·</span>Breaking-Change Governance</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-22 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0007 refined NetKingdom's orchestration role into a meta-orchestration layer. NetKingdom selects the services and playbooks a scenario needs, decides which parameters may be tuned, and holds the responsibility map. Railiance remains the execution-orchestration layer: Railiance playbooks provision and converge the actual infrastructure, cluster, platform services, and application layers.</p>
|
||||
<p>That split requires a stable interface. If a Railiance playbook only describes behavior implicitly, NetKingdom cannot safely compose it into a scenario, compare it with another playbook, or know which parameter changes are safe. The IAM Profile provides the precedent: the consumer that needs a stable contract defines the contract, and providers conform to it.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>NetKingdom owns the Playbook Capability Contract schema and vocabulary. Railiance owns playbook implementation and publishes one conformant declaration per playbook.</p>
|
||||
<p>The first canonical contract is <code>canon/standards/playbook-capability-contract_v0.1.md</code>, backed by the machine-readable schema in <code>canon/schemas/playbook-capability-declaration_v0.1.schema.json</code> and the validator in <code>tools/playbook-capability-contract/</code>.</p>
|
||||
<p>The contract is NetKingdom-owned with Railiance co-design:</p>
|
||||
<ul><li>NetKingdom defines the schema, controlled vocabulary, trust-state language, parameter-sensitivity rules, and conformance criteria.</li><li>Railiance authors and maintains declarations beside the playbooks they describe.</li><li>Railiance execution stays unchanged. The declaration never becomes the playbook runner.</li><li>NetKingdom meta-orchestration consumes declarations to select, parametrize, sequence, and build responsibility maps for scenarios.</li></ul>
|
||||
<p>ADR-0007 remains unchanged: execution stays in Railiance.</p>
|
||||
</section>
|
||||
<section id="versioning"><h2>Versioning</h2>
|
||||
<p>The contract uses explicit document versions:</p>
|
||||
<ul><li>Patch/editorial changes clarify wording or examples without changing declaration semantics.</li><li>Minor versions add optional fields, vocabulary entries, or validator warnings that existing declarations can ignore.</li><li>Breaking versions change required fields, field meanings, allowed vocabulary, parameter-sensitivity semantics, trust-state semantics, or catalog consumption rules.</li></ul>
|
||||
<p>Declarations carry <code>metadata.contract_version</code>. A catalog may accept more than one contract version during a migration window, but must report the version used for each selected playbook.</p>
|
||||
</section>
|
||||
<section id="breaking-change-governance"><h2>Breaking-Change Governance</h2>
|
||||
<p>A breaking change requires:</p>
|
||||
<ol><li>an ADR or ADR refinement explaining the change and migration path;</li><li>a new versioned standard and schema;</li><li>an updated validator;</li><li>a coexistence window for the previous supported version where practical;</li><li>notice to known declaration publishers, especially Railiance repos.</li></ol>
|
||||
<p>Breaking changes include:</p>
|
||||
<ul><li>removing or renaming required fields;</li><li>changing capability ids or resource-kind vocabulary;</li><li>changing trust-state meanings;</li><li>changing which parameter sensitivities are tenant-tunable;</li><li>changing catalog selection or override semantics;</li><li>moving execution responsibility out of Railiance into NetKingdom.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Playbook declaration files live beside Railiance playbooks, normally at <code>capabilities/playbooks/*.yaml</code>.</li><li>NetKingdom can validate declarations before consuming them.</li><li>A playbook interface change becomes visible and versioned instead of an implicit break.</li><li>The responsibility map can be assembled from declarations, while Railiance keeps execution ownership.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Put The Contract In Railiance</h3>
|
||||
<p>Railiance owns execution, so this is tempting. But NetKingdom is the consumer that needs stable scenario composition and responsibility-map inputs. Keeping the contract in NetKingdom mirrors the IAM Profile pattern and keeps scenario semantics close to the responsibility map.</p>
|
||||
<h3>Make Declarations Free-Form Documentation</h3>
|
||||
<p>Free-form docs are readable but not safely composable. NetKingdom needs a validator and controlled vocabulary so a playbook change cannot silently break a scenario.</p>
|
||||
<h3>Build A Dedicated Execution-Orchestration Repo Now</h3>
|
||||
<p>ADR-0007 explicitly defers that. The contract is useful now and does not require a new runner or repo boundary.</p>
|
||||
</section><footer><span>NK-ADR-0012 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0012-playbook-capability-contract-ownership.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="e269fabfc376f97f2a03ea66e34059d54016a445a7f30b351a6774b65c96c2ee">
|
||||
<title>Playbook Capability Contract Ownership</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0012</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Playbook Capability Contract Ownership</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0012-playbook-capability-contract-ownership.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#versioning"><span class="n">·</span>Versioning</a></li><li><a href="#breaking-change-governance"><span class="n">·</span>Breaking-Change Governance</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-22 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0007 refined NetKingdom's orchestration role into a meta-orchestration layer. NetKingdom selects the services and playbooks a scenario needs, decides which parameters may be tuned, and holds the responsibility map. Railiance remains the execution-orchestration layer: Railiance playbooks provision and converge the actual infrastructure, cluster, platform services, and application layers.</p>
|
||||
<p>That split requires a stable interface. If a Railiance playbook only describes behavior implicitly, NetKingdom cannot safely compose it into a scenario, compare it with another playbook, or know which parameter changes are safe. The IAM Profile provides the precedent: the consumer that needs a stable contract defines the contract, and providers conform to it.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>NetKingdom owns the Playbook Capability Contract schema and vocabulary. Railiance owns playbook implementation and publishes one conformant declaration per playbook.</p>
|
||||
<p>The first canonical contract is <code>canon/standards/playbook-capability-contract_v0.1.md</code>, backed by the machine-readable schema in <code>canon/schemas/playbook-capability-declaration_v0.1.schema.json</code> and the validator in <code>tools/playbook-capability-contract/</code>.</p>
|
||||
<p>The contract is NetKingdom-owned with Railiance co-design:</p>
|
||||
<ul><li>NetKingdom defines the schema, controlled vocabulary, trust-state language, parameter-sensitivity rules, and conformance criteria.</li><li>Railiance authors and maintains declarations beside the playbooks they describe.</li><li>Railiance execution stays unchanged. The declaration never becomes the playbook runner.</li><li>NetKingdom meta-orchestration consumes declarations to select, parametrize, sequence, and build responsibility maps for scenarios.</li></ul>
|
||||
<p>ADR-0007 remains unchanged: execution stays in Railiance.</p>
|
||||
</section>
|
||||
<section id="versioning"><h2>Versioning</h2>
|
||||
<p>The contract uses explicit document versions:</p>
|
||||
<ul><li>Patch/editorial changes clarify wording or examples without changing declaration semantics.</li><li>Minor versions add optional fields, vocabulary entries, or validator warnings that existing declarations can ignore.</li><li>Breaking versions change required fields, field meanings, allowed vocabulary, parameter-sensitivity semantics, trust-state semantics, or catalog consumption rules.</li></ul>
|
||||
<p>Declarations carry <code>metadata.contract_version</code>. A catalog may accept more than one contract version during a migration window, but must report the version used for each selected playbook.</p>
|
||||
</section>
|
||||
<section id="breaking-change-governance"><h2>Breaking-Change Governance</h2>
|
||||
<p>A breaking change requires:</p>
|
||||
<ol><li>an ADR or ADR refinement explaining the change and migration path;</li><li>a new versioned standard and schema;</li><li>an updated validator;</li><li>a coexistence window for the previous supported version where practical;</li><li>notice to known declaration publishers, especially Railiance repos.</li></ol>
|
||||
<p>Breaking changes include:</p>
|
||||
<ul><li>removing or renaming required fields;</li><li>changing capability ids or resource-kind vocabulary;</li><li>changing trust-state meanings;</li><li>changing which parameter sensitivities are tenant-tunable;</li><li>changing catalog selection or override semantics;</li><li>moving execution responsibility out of Railiance into NetKingdom.</li></ul>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Playbook declaration files live beside Railiance playbooks, normally at <code>capabilities/playbooks/*.yaml</code>.</li><li>NetKingdom can validate declarations before consuming them.</li><li>A playbook interface change becomes visible and versioned instead of an implicit break.</li><li>The responsibility map can be assembled from declarations, while Railiance keeps execution ownership.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Put The Contract In Railiance</h3>
|
||||
<p>Railiance owns execution, so this is tempting. But NetKingdom is the consumer that needs stable scenario composition and responsibility-map inputs. Keeping the contract in NetKingdom mirrors the IAM Profile pattern and keeps scenario semantics close to the responsibility map.</p>
|
||||
<h3>Make Declarations Free-Form Documentation</h3>
|
||||
<p>Free-form docs are readable but not safely composable. NetKingdom needs a validator and controlled vocabulary so a playbook change cannot silently break a scenario.</p>
|
||||
<h3>Build A Dedicated Execution-Orchestration Repo Now</h3>
|
||||
<p>ADR-0007 explicitly defers that. The contract is useful now and does not require a new runner or repo boundary.</p>
|
||||
</section><footer><span>NK-ADR-0012 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0012-playbook-capability-contract-ownership.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
229
build/adr/netkingdom-railiance-workload-packaging/v1/index.html
Normal file
229
build/adr/netkingdom-railiance-workload-packaging/v1/index.html
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="2000985ef211aeedd3656e15cedb289e655526c2dcc4a161a64ffc27f0289db1">
|
||||
<title>NetKingdom Railiance Workload Packaging and Relational Platform</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0015</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom Railiance Workload Packaging and Relational Platform</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-08-11 <strong>Deciders:</strong> Bernd Worsch, Claude</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>NetKingdom's runtime services are deployed today outside the Railiance reef/rail/<code>rapp</code> model. <code>tenant-engine</code> and <code>user-engine</code> run on the coulomb substrate with digest-pinned images (<code>forgejo.coulomb.social/coulomb/{tenant,user}-engine@sha256:…</code>), but their Kubernetes manifests live in this repo at <code>sso-mfa/k8s/<service>/runtime.yaml</code> — a canon repo holding runtime YAML — applied imperatively, with <code>verify-t0*.sh</code> scripts as verification. Neither service declares <code>railiance/app.toml</code>, appears in <code>reef-railiance/bindings/rapps.yaml</code>, or is reconciled by a GitOps controller.</p>
|
||||
<p>The decision to bring NetKingdom under Railiance governance forces two questions this ADR settles.</p>
|
||||
<p><strong>Packaging granularity.</strong> <code>railiance-master/docs/repository-axes.md</code> maps <code>rapp-*</code> to "one Railiance-managed workload" and states that a <code>rapp</code> "does not replace the responsibility repo that owns the broader domain. It is explicitly about managed wrapping, not ownership." Precedent exists in two shapes: <code>rapp-qonto</code> (<code>ownership_repo: qonto-assistant</code>, one workload, thin package repo pointing back at the domain repo) and <code>rapp-postgres</code> (<code>ownership_repo: railiance-platform</code>, one workload with a <code>consumers:</code> list and a <code>consumer_contract</code>). The open question was whether NetKingdom's engines are one workload or several.</p>
|
||||
<p><strong>Relational storage.</strong> The engines have diverged. <code>user-engine</code> already runs CloudNative PG (<code>kind: Cluster</code> in its own namespace) with NetworkPolicies and a <code>migrations/</code> surface. <code>tenant-engine</code> runs SQLite on a 1 Gi RWO PVC, chosen in TEN-WP-0004 for expedience. <code>railiance-platform</code> (S3) declares CloudNative PG the canonical database operator, and <code>rapp-postgres</code> packages it with a <code>database-per-consumer</code> boundary unit and OpenBao dynamic credentials. Separately, <code>rail-kubernetes</code>'s wave-1 contract explicitly states the rail must <strong>not</strong> assume "a generic persistent-storage contract" — so a per-workload PVC is unsupported by the rail regardless of which database is chosen.</p>
|
||||
<p>TEN-WP-0005 was drafted against PostgreSQL and implemented against SQLite, because T05 must roll out against the runtime that exists. That workplan's <code>TenantStore</code> Protocol was kept as an explicit seam for this decision.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. Separate <code>rapp-*</code> repos per engine</h3>
|
||||
<p>Each deployed NetKingdom engine gets its own managed workload package repo:</p>
|
||||
<pre>rapp-tenant-engine ownership_repo: tenant-engine
|
||||
rapp-user-engine ownership_repo: user-engine</pre>
|
||||
<p>Following <code>rapp-qonto</code>'s shape: the <code>rapp</code> owns Railiance packaging, rail compatibility, workload-specific smoke/health checks, rollout and rollback expectations, secret references, and dependency declarations. The engine repo retains domain ownership; <code>net-kingdom</code> retains canon.</p>
|
||||
<p>This follows the one-workload rule rather than working around it. The engines are already independent on every axis a <code>rapp</code> declaration must state: separate namespaces, separate image digests, separate release cadence, and (until decision 2 lands) different storage. A single <code>rapp-netkingdom</code> would have to declare one <code>workload_identity</code>, one <code>rollout_contract</code>, and one <code>rollback_contract</code> across services that genuinely differ.</p>
|
||||
<p>The decisive property is <strong>independent rollback</strong>: rolling back a bad <code>tenant-engine</code> revision must not force a <code>user-engine</code> revision change. A single rapp would make that either impossible or fictional.</p>
|
||||
<p>Rollout ordering between the engines is a declared constraint between rapps, not a reason to merge them: <code>tenant-engine</code> before <code>user-engine</code>, following the dependency direction.</p>
|
||||
<h3>2. <code>secrets-engine</code> is not packaged as a <code>rapp</code></h3>
|
||||
<p><code>secrets-engine</code> has no deployed workload — no Containerfile, no Makefile, no Kubernetes manifests. It is a catalog/policy/workflow layer over OpenBao, whose packaging already belongs to <code>rapp-openbao</code> with custody and lane policy in <code>railiance-platform</code>.</p>
|
||||
<p>A <code>rapp</code> wraps a workload; <code>secrets-engine</code> has none to wrap. Revisit only if it becomes a runtime service, at which point this decision reopens for that repo alone and not for the packaging model.</p>
|
||||
<h3>3. CloudNative PG is the default relational platform for production</h3>
|
||||
<p>Production NetKingdom services requiring relational storage use CloudNative PG via <code>rapp-postgres</code>, consuming the <code>database-per-consumer</code> boundary unit and the <code>openbao-dynamic-database-credential</code> lane, with tenant-keying per <code>business-app-service-contract_v0.1</code> section 1.3.</p>
|
||||
<p>Per-workload SQLite-on-a-PVC is not a production pattern. It remains acceptable for local development and tests.</p>
|
||||
<p><code>tenant-engine</code> migrates from SQLite to cnpg. Its <code>TenantStore</code> Protocol makes this a backend swap behind an existing seam rather than a rewrite; the lifecycle semantics proven in TEN-WP-0005 (atomic compare-and-swap, durable idempotency receipts, forward-only migration) are the conformance bar the PostgreSQL backend must meet, and its store-conformance suite is already parametrised across backends to enforce exactly that.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Two new <code>rapp-*</code> repos to create, each requiring a <code>declarations/rapp.yaml</code>, a binding in <code>reef-railiance/bindings/rapps.yaml</code>, and its own readiness evidence progression (<code>declared</code> → <code>installed</code> → <code>verified</code> → <code>production-approved</code>).</li><li>Per-binding evidence multiplies, but per-reef evidence does not — the <code>reef-production-readiness-contract</code> puts substrate, ingress, storage, network, and backup evidence on the reef, once. What multiplies is the critical-workload gate (threat model, negative authorization tests, rollback rules, residual-risk owner), which identity-plane services warrant individually. A shared NetKingdom threat model may be referenced by both bindings rather than duplicated.</li><li>Runtime manifests move out of <code>net-kingdom/sso-mfa/k8s/</code> into the respective <code>rapp</code> repos. This repo stops holding runtime YAML and returns to canon, standards, and ADRs.</li><li><code>tenant-engine</code> requires a data migration from SQLite to cnpg, including migration of existing tenants, grants, plan assignments, and idempotency receipts. TEN-WP-0005-T05's rollout plan is affected: it currently targets the SQLite runtime.</li><li>NetKingdom services become subject to the <code>railiance/app.toml</code> staged promotion contract (Stage 1 local → Stage 2 canary → Stage 3 production), including declared rollback commands and health endpoints. Digest-pinned images already satisfy <code>digest_policy = "required"</code>.</li><li>The reef must accept or mitigate its single-server and shared-control-plane risk for each NetKingdom binding, as it must for <code>rapp-qonto</code>. The reef name and a <code>high</code> criticality label are not evidence.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<p><strong>One <code>rapp-netkingdom</code> for all associated repos.</strong> Rejected. It would reduce binding-evidence count and match the current coupled deployment (both engines' manifests share one tree; <code>flex-auth</code> runs per-consumer instances <code>flex-auth-tenant-engine</code> and <code>flex-auth-user-engine</code>). But it contradicts the one-workload rule in <code>repository-axes.md</code>, and would force a single rollback contract across independently versioned services. A rollback contract that depends on which service failed is not a contract. This alternative would be correct only if the engines were always promoted and rolled back as one atomic cutover — which their separate digests and release cadences contradict.</p>
|
||||
<p><strong>Keep SQLite for <code>tenant-engine</code>, standardise later.</strong> Rejected. It leaves the two engines operationally dissimilar, which is the opposite of the intent, and <code>rail-kubernetes</code> does not support the per-workload persistent-storage contract the PVC depends on. Deferring also grows the migration: every tenant, grant, and receipt written between now and the cutover is data to move.</p>
|
||||
<p><strong>A <code>rapp</code> per NetKingdom concern with a <code>consumers:</code> list, mirroring <code>rapp-postgres</code>.</strong> Rejected as a category error. <code>rapp-postgres</code>'s consumers are consumers <em>of one workload</em> (PostgreSQL); NetKingdom's engines are distinct workloads, not consumers of a shared one.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li>Create <code>rapp-tenant-engine</code> and <code>rapp-user-engine</code>; move runtime manifests out of <code>net-kingdom/sso-mfa/k8s/</code>.</li><li>Add both bindings to <code>reef-railiance/bindings/rapps.yaml</code> at <code>declared</code>.</li><li>Add <code>railiance/app.toml</code> to <code>tenant-engine</code> and <code>user-engine</code>.</li><li>Open a <code>tenant-engine</code> workplan for the cnpg backend and data migration; reconcile with TEN-WP-0005-T05, whose rollout currently targets SQLite.</li><li>Confirm whether <code>secrets-engine</code> is intended to remain a non-deployed control layer. This ADR assumes it is.</li><li>Record the NetKingdom-wide threat model that both bindings will reference.</li></ul>
|
||||
</section><footer><span>NK-ADR-0015 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="2000985ef211aeedd3656e15cedb289e655526c2dcc4a161a64ffc27f0289db1">
|
||||
<title>NetKingdom Railiance Workload Packaging and Relational Platform</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0015</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom Railiance Workload Packaging and Relational Platform</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-08-11 <strong>Deciders:</strong> Bernd Worsch, Claude</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>NetKingdom's runtime services are deployed today outside the Railiance reef/rail/<code>rapp</code> model. <code>tenant-engine</code> and <code>user-engine</code> run on the coulomb substrate with digest-pinned images (<code>forgejo.coulomb.social/coulomb/{tenant,user}-engine@sha256:…</code>), but their Kubernetes manifests live in this repo at <code>sso-mfa/k8s/<service>/runtime.yaml</code> — a canon repo holding runtime YAML — applied imperatively, with <code>verify-t0*.sh</code> scripts as verification. Neither service declares <code>railiance/app.toml</code>, appears in <code>reef-railiance/bindings/rapps.yaml</code>, or is reconciled by a GitOps controller.</p>
|
||||
<p>The decision to bring NetKingdom under Railiance governance forces two questions this ADR settles.</p>
|
||||
<p><strong>Packaging granularity.</strong> <code>railiance-master/docs/repository-axes.md</code> maps <code>rapp-*</code> to "one Railiance-managed workload" and states that a <code>rapp</code> "does not replace the responsibility repo that owns the broader domain. It is explicitly about managed wrapping, not ownership." Precedent exists in two shapes: <code>rapp-qonto</code> (<code>ownership_repo: qonto-assistant</code>, one workload, thin package repo pointing back at the domain repo) and <code>rapp-postgres</code> (<code>ownership_repo: railiance-platform</code>, one workload with a <code>consumers:</code> list and a <code>consumer_contract</code>). The open question was whether NetKingdom's engines are one workload or several.</p>
|
||||
<p><strong>Relational storage.</strong> The engines have diverged. <code>user-engine</code> already runs CloudNative PG (<code>kind: Cluster</code> in its own namespace) with NetworkPolicies and a <code>migrations/</code> surface. <code>tenant-engine</code> runs SQLite on a 1 Gi RWO PVC, chosen in TEN-WP-0004 for expedience. <code>railiance-platform</code> (S3) declares CloudNative PG the canonical database operator, and <code>rapp-postgres</code> packages it with a <code>database-per-consumer</code> boundary unit and OpenBao dynamic credentials. Separately, <code>rail-kubernetes</code>'s wave-1 contract explicitly states the rail must <strong>not</strong> assume "a generic persistent-storage contract" — so a per-workload PVC is unsupported by the rail regardless of which database is chosen.</p>
|
||||
<p>TEN-WP-0005 was drafted against PostgreSQL and implemented against SQLite, because T05 must roll out against the runtime that exists. That workplan's <code>TenantStore</code> Protocol was kept as an explicit seam for this decision.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<h3>1. Separate <code>rapp-*</code> repos per engine</h3>
|
||||
<p>Each deployed NetKingdom engine gets its own managed workload package repo:</p>
|
||||
<pre>rapp-tenant-engine ownership_repo: tenant-engine
|
||||
rapp-user-engine ownership_repo: user-engine</pre>
|
||||
<p>Following <code>rapp-qonto</code>'s shape: the <code>rapp</code> owns Railiance packaging, rail compatibility, workload-specific smoke/health checks, rollout and rollback expectations, secret references, and dependency declarations. The engine repo retains domain ownership; <code>net-kingdom</code> retains canon.</p>
|
||||
<p>This follows the one-workload rule rather than working around it. The engines are already independent on every axis a <code>rapp</code> declaration must state: separate namespaces, separate image digests, separate release cadence, and (until decision 2 lands) different storage. A single <code>rapp-netkingdom</code> would have to declare one <code>workload_identity</code>, one <code>rollout_contract</code>, and one <code>rollback_contract</code> across services that genuinely differ.</p>
|
||||
<p>The decisive property is <strong>independent rollback</strong>: rolling back a bad <code>tenant-engine</code> revision must not force a <code>user-engine</code> revision change. A single rapp would make that either impossible or fictional.</p>
|
||||
<p>Rollout ordering between the engines is a declared constraint between rapps, not a reason to merge them: <code>tenant-engine</code> before <code>user-engine</code>, following the dependency direction.</p>
|
||||
<h3>2. <code>secrets-engine</code> is not packaged as a <code>rapp</code></h3>
|
||||
<p><code>secrets-engine</code> has no deployed workload — no Containerfile, no Makefile, no Kubernetes manifests. It is a catalog/policy/workflow layer over OpenBao, whose packaging already belongs to <code>rapp-openbao</code> with custody and lane policy in <code>railiance-platform</code>.</p>
|
||||
<p>A <code>rapp</code> wraps a workload; <code>secrets-engine</code> has none to wrap. Revisit only if it becomes a runtime service, at which point this decision reopens for that repo alone and not for the packaging model.</p>
|
||||
<h3>3. CloudNative PG is the default relational platform for production</h3>
|
||||
<p>Production NetKingdom services requiring relational storage use CloudNative PG via <code>rapp-postgres</code>, consuming the <code>database-per-consumer</code> boundary unit and the <code>openbao-dynamic-database-credential</code> lane, with tenant-keying per <code>business-app-service-contract_v0.1</code> section 1.3.</p>
|
||||
<p>Per-workload SQLite-on-a-PVC is not a production pattern. It remains acceptable for local development and tests.</p>
|
||||
<p><code>tenant-engine</code> migrates from SQLite to cnpg. Its <code>TenantStore</code> Protocol makes this a backend swap behind an existing seam rather than a rewrite; the lifecycle semantics proven in TEN-WP-0005 (atomic compare-and-swap, durable idempotency receipts, forward-only migration) are the conformance bar the PostgreSQL backend must meet, and its store-conformance suite is already parametrised across backends to enforce exactly that.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Two new <code>rapp-*</code> repos to create, each requiring a <code>declarations/rapp.yaml</code>, a binding in <code>reef-railiance/bindings/rapps.yaml</code>, and its own readiness evidence progression (<code>declared</code> → <code>installed</code> → <code>verified</code> → <code>production-approved</code>).</li><li>Per-binding evidence multiplies, but per-reef evidence does not — the <code>reef-production-readiness-contract</code> puts substrate, ingress, storage, network, and backup evidence on the reef, once. What multiplies is the critical-workload gate (threat model, negative authorization tests, rollback rules, residual-risk owner), which identity-plane services warrant individually. A shared NetKingdom threat model may be referenced by both bindings rather than duplicated.</li><li>Runtime manifests move out of <code>net-kingdom/sso-mfa/k8s/</code> into the respective <code>rapp</code> repos. This repo stops holding runtime YAML and returns to canon, standards, and ADRs.</li><li><code>tenant-engine</code> requires a data migration from SQLite to cnpg, including migration of existing tenants, grants, plan assignments, and idempotency receipts. TEN-WP-0005-T05's rollout plan is affected: it currently targets the SQLite runtime.</li><li>NetKingdom services become subject to the <code>railiance/app.toml</code> staged promotion contract (Stage 1 local → Stage 2 canary → Stage 3 production), including declared rollback commands and health endpoints. Digest-pinned images already satisfy <code>digest_policy = "required"</code>.</li><li>The reef must accept or mitigate its single-server and shared-control-plane risk for each NetKingdom binding, as it must for <code>rapp-qonto</code>. The reef name and a <code>high</code> criticality label are not evidence.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<p><strong>One <code>rapp-netkingdom</code> for all associated repos.</strong> Rejected. It would reduce binding-evidence count and match the current coupled deployment (both engines' manifests share one tree; <code>flex-auth</code> runs per-consumer instances <code>flex-auth-tenant-engine</code> and <code>flex-auth-user-engine</code>). But it contradicts the one-workload rule in <code>repository-axes.md</code>, and would force a single rollback contract across independently versioned services. A rollback contract that depends on which service failed is not a contract. This alternative would be correct only if the engines were always promoted and rolled back as one atomic cutover — which their separate digests and release cadences contradict.</p>
|
||||
<p><strong>Keep SQLite for <code>tenant-engine</code>, standardise later.</strong> Rejected. It leaves the two engines operationally dissimilar, which is the opposite of the intent, and <code>rail-kubernetes</code> does not support the per-workload persistent-storage contract the PVC depends on. Deferring also grows the migration: every tenant, grant, and receipt written between now and the cutover is data to move.</p>
|
||||
<p><strong>A <code>rapp</code> per NetKingdom concern with a <code>consumers:</code> list, mirroring <code>rapp-postgres</code>.</strong> Rejected as a category error. <code>rapp-postgres</code>'s consumers are consumers <em>of one workload</em> (PostgreSQL); NetKingdom's engines are distinct workloads, not consumers of a shared one.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li>Create <code>rapp-tenant-engine</code> and <code>rapp-user-engine</code>; move runtime manifests out of <code>net-kingdom/sso-mfa/k8s/</code>.</li><li>Add both bindings to <code>reef-railiance/bindings/rapps.yaml</code> at <code>declared</code>.</li><li>Add <code>railiance/app.toml</code> to <code>tenant-engine</code> and <code>user-engine</code>.</li><li>Open a <code>tenant-engine</code> workplan for the cnpg backend and data migration; reconcile with TEN-WP-0005-T05, whose rollout currently targets SQLite.</li><li>Confirm whether <code>secrets-engine</code> is intended to remain a non-deployed control layer. This ADR assumes it is.</li><li>Record the NetKingdom-wide threat model that both bindings will reference.</li></ul>
|
||||
</section><footer><span>NK-ADR-0015 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="e92a43649bb6e14e53ec62ecc819405bf3a44bda9557487f7177402f107bbd13">
|
||||
<title>Recursive Multi-Tenant Identity and Authorization Architecture</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0006</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Recursive Multi-Tenant Identity and Authorization Architecture</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-17 <strong>Deciders:</strong> Bernd Worsch</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The Coulomb platform is being built from the same repositories and services that will later support other use cases. This creates a recursive architecture problem: Coulomb needs to use the shared identity, security, policy, and deployment capabilities, while those capabilities are themselves part of the infrastructure being built.</p>
|
||||
<p>If this recursion is left implicit, the first internal use case can drift into being treated as the platform root of trust. That would make future multi-tenant use harder, blur operational authority, and make secure bootstrap/recovery decisions harder to reason about.</p>
|
||||
<p>NetKingdom already owns identity and security architecture concerns. key-cape provides a lightweight IAM implementation of the NetKingdom IAM Profile. Keycloak remains the expanded production IAM option. privacyIDEA is relevant for MFA/token lifecycle. flex-auth is emerging as the canonical authorization control plane and practical reference implementation of CARING authorization semantics. Topaz is the most likely first delegated authorization runtime behind flex-auth.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>We will document and implement the platform security architecture as a recursive multi-tenant architecture with three explicit planes:</p>
|
||||
<ul><li><strong>Bootstrap plane</strong> - establishes the first trusted runtime and recovery authority before normal platform services exist.</li><li><strong>Platform control plane</strong> - operates shared identity, MFA, secrets, authorization, policy, audit, and explanation services.</li><li><strong>Tenant plane</strong> - runs Coulomb and future workloads under scoped tenant authority.</li></ul>
|
||||
<p>Coulomb will be treated as the first internal/reference tenant, not as the platform root of trust.</p>
|
||||
<p>NetKingdom will own the canonical security architecture and standards. Railiance will own deployment layering and orchestration boundaries. flex-auth will own the canonical authorization interface and CARING-based policy/decision model. Topaz will be the first delegated PDP runtime, with other authorization engines treated as adapters where useful.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Architecture documentation must separate platform-root authority from tenant administration, even for Coulomb.</li><li>Workplans for identity, authorization, and bootstrapping must include explicit tenant and control-plane boundaries.</li><li>Bootstrap design must include trust-state transitions and recovery procedures rather than assuming the final IAM service already exists.</li><li>flex-auth should model tenants, platform resources, CARING descriptors, decision envelopes, and runtime adapters in a provider-neutral way.</li><li>key-cape and Keycloak should be treated as implementations of the IAM Profile, not as the canonical source of resource authorization semantics.</li><li>A future orchestration repo may be useful, but only to coordinate safe sequencing across Railiance and NetKingdom capabilities. It must not bypass Railiance stack ownership.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Treat Coulomb As The Platform Root</h3>
|
||||
<p>This is simpler during early development but creates long-term coupling between one internal use case and the shared platform. It makes later multi-tenant operation and secure bootstrap harder.</p>
|
||||
<h3>Put All Security Semantics Into Keycloak</h3>
|
||||
<p>Keycloak is useful for expanded IAM and can provide authorization features, but making it the canonical model would make lightweight mode and future authorization backends harder to support. The preferred model keeps identity provider concerns separate from canonical authorization semantics.</p>
|
||||
<h3>Create An Orchestration Repo Immediately</h3>
|
||||
<p>A dedicated orchestration repo may become appropriate. Creating it before we define trust states and repo boundaries would risk encoding accidental sequence logic too early. The immediate step is to document the state machine and update workplans.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li>Refine bootstrapping around explicit trust-state transitions.</li><li>Add tenant/control-plane language to flex-auth authorization workplans.</li><li>Define the first production Topaz integration boundary for flex-auth.</li><li>Decide when key-cape is sufficient and when Keycloak expanded mode is required.</li><li>Decide what, if anything, should live in a future orchestration repo.</li></ul>
|
||||
</section><footer><span>NK-ADR-0006 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="e92a43649bb6e14e53ec62ecc819405bf3a44bda9557487f7177402f107bbd13">
|
||||
<title>Recursive Multi-Tenant Identity and Authorization Architecture</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0006</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Recursive Multi-Tenant Identity and Authorization Architecture</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-17 <strong>Deciders:</strong> Bernd Worsch</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The Coulomb platform is being built from the same repositories and services that will later support other use cases. This creates a recursive architecture problem: Coulomb needs to use the shared identity, security, policy, and deployment capabilities, while those capabilities are themselves part of the infrastructure being built.</p>
|
||||
<p>If this recursion is left implicit, the first internal use case can drift into being treated as the platform root of trust. That would make future multi-tenant use harder, blur operational authority, and make secure bootstrap/recovery decisions harder to reason about.</p>
|
||||
<p>NetKingdom already owns identity and security architecture concerns. key-cape provides a lightweight IAM implementation of the NetKingdom IAM Profile. Keycloak remains the expanded production IAM option. privacyIDEA is relevant for MFA/token lifecycle. flex-auth is emerging as the canonical authorization control plane and practical reference implementation of CARING authorization semantics. Topaz is the most likely first delegated authorization runtime behind flex-auth.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>We will document and implement the platform security architecture as a recursive multi-tenant architecture with three explicit planes:</p>
|
||||
<ul><li><strong>Bootstrap plane</strong> - establishes the first trusted runtime and recovery authority before normal platform services exist.</li><li><strong>Platform control plane</strong> - operates shared identity, MFA, secrets, authorization, policy, audit, and explanation services.</li><li><strong>Tenant plane</strong> - runs Coulomb and future workloads under scoped tenant authority.</li></ul>
|
||||
<p>Coulomb will be treated as the first internal/reference tenant, not as the platform root of trust.</p>
|
||||
<p>NetKingdom will own the canonical security architecture and standards. Railiance will own deployment layering and orchestration boundaries. flex-auth will own the canonical authorization interface and CARING-based policy/decision model. Topaz will be the first delegated PDP runtime, with other authorization engines treated as adapters where useful.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Architecture documentation must separate platform-root authority from tenant administration, even for Coulomb.</li><li>Workplans for identity, authorization, and bootstrapping must include explicit tenant and control-plane boundaries.</li><li>Bootstrap design must include trust-state transitions and recovery procedures rather than assuming the final IAM service already exists.</li><li>flex-auth should model tenants, platform resources, CARING descriptors, decision envelopes, and runtime adapters in a provider-neutral way.</li><li>key-cape and Keycloak should be treated as implementations of the IAM Profile, not as the canonical source of resource authorization semantics.</li><li>A future orchestration repo may be useful, but only to coordinate safe sequencing across Railiance and NetKingdom capabilities. It must not bypass Railiance stack ownership.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Treat Coulomb As The Platform Root</h3>
|
||||
<p>This is simpler during early development but creates long-term coupling between one internal use case and the shared platform. It makes later multi-tenant operation and secure bootstrap harder.</p>
|
||||
<h3>Put All Security Semantics Into Keycloak</h3>
|
||||
<p>Keycloak is useful for expanded IAM and can provide authorization features, but making it the canonical model would make lightweight mode and future authorization backends harder to support. The preferred model keeps identity provider concerns separate from canonical authorization semantics.</p>
|
||||
<h3>Create An Orchestration Repo Immediately</h3>
|
||||
<p>A dedicated orchestration repo may become appropriate. Creating it before we define trust states and repo boundaries would risk encoding accidental sequence logic too early. The immediate step is to document the state machine and update workplans.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li>Refine bootstrapping around explicit trust-state transitions.</li><li>Add tenant/control-plane language to flex-auth authorization workplans.</li><li>Define the first production Topaz integration boundary for flex-auth.</li><li>Decide when key-cape is sufficient and when Keycloak expanded mode is required.</li><li>Decide what, if anything, should live in a future orchestration repo.</li></ul>
|
||||
</section><footer><span>NK-ADR-0006 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="b4fcff8448f07aca1fcb6908618bdb19f6c0e7dce25d4c5c8b85eec2f175233b">
|
||||
<title>Security Orchestration Boundary</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0007</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Security Orchestration Boundary</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0007-security-orchestration-boundary.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#future-repo-trigger"><span class="n">·</span>Future Repo Trigger</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#refinement-2026-05-21-meta-orchestration-layer"><span class="n">·</span>Refinement (2026-05-21): Meta-Orchestration Layer</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-18 <strong>Refined:</strong> 2026-05-21 (meta-orchestration layer — see below) <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The recursive platform security architecture needs careful sequencing: host trust, cluster trust, bootstrap secrets, runtime secret authority, runtime identity, runtime authorization, tenant onboarding, and readiness verification.</p>
|
||||
<p>That sequencing crosses NetKingdom and Railiance ownership boundaries. NetKingdom owns the canonical security architecture, IAM Profile, credential/bootstrap standards, and authorization semantics. Railiance owns deployment layering for infrastructure, clusters, platform services, and applications. OpenBao adds an important runtime-secret authority to the platform control plane, but it does not change those ownership boundaries.</p>
|
||||
<p>Creating a dedicated orchestration repo too early would risk encoding temporary bootstrap order and accidental stack assumptions as a permanent interface. Leaving every sequence implicit would also be risky: platform root actions, OpenBao initialization, policy import, and tenant onboarding must be auditable and repeatable.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Security orchestration will stay in Railiance playbooks for now.</p>
|
||||
<p>NetKingdom will define the trust-state model, readiness checks, policy semantics, OpenBao boundaries, and tenant/control-plane rules. Railiance playbooks will own the concrete deployment sequencing across <code>railiance-infra</code>, <code>railiance-cluster</code>, <code>railiance-platform</code>, and <code>railiance-apps</code>.</p>
|
||||
<p>A dedicated orchestration repo is deferred until the sequencing surface is stable enough to justify its own product boundary. If created later, it must coordinate safe sequencing and readiness reporting; it must not own security policy semantics or bypass Railiance stack ownership.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>NK-WP-0006 is implemented as architecture, standards, ADRs, and workplan constraints rather than a new repo.</li><li>OpenBao bootstrap, unseal/recovery, audit, backup, and workload-secret delivery belong in Railiance platform playbooks, governed by NetKingdom standards.</li><li>Cross-repo readiness should be reported as checks against explicit trust states, not as a hidden imperative script.</li><li>A future orchestration repo needs a new ADR before creation.</li></ul>
|
||||
</section>
|
||||
<section id="future-repo-trigger"><h2>Future Repo Trigger</h2>
|
||||
<p>Revisit a dedicated orchestration repo only if at least two of these are true:</p>
|
||||
<ul><li>multiple Railiance deployments need the same security sequencing interface;</li><li>readiness reporting becomes a reusable artifact consumed by operators, agents, or CI;</li><li>rollback and recovery workflows need a cross-repo state machine that no single Railiance layer can own cleanly;</li><li>tenant onboarding becomes a repeatable workflow spanning identity, flex-auth, Topaz, OpenBao, object storage, and application repos.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Create A Dedicated Orchestration Repo Now</h3>
|
||||
<p>This would give sequencing a visible home, but it would probably encode unstable details before OpenBao runtime operations, flex-auth/Topaz policy import, and tenant onboarding have enough implementation feedback.</p>
|
||||
<h3>Put Orchestration In NetKingdom</h3>
|
||||
<p>NetKingdom owns the security model, but it should not become the deployment repo for every stack layer. This would blur architecture ownership with platform deployment ownership.</p>
|
||||
<h3>Leave Sequencing Entirely Informal</h3>
|
||||
<p>This avoids premature structure but leaves bootstrap and runtime trust transitions too dependent on operator memory. The accepted approach keeps the sequence explicit while leaving concrete deployment in the Railiance stack.</p>
|
||||
</section>
|
||||
<section id="refinement-2026-05-21-meta-orchestration-layer"><h2>Refinement (2026-05-21): Meta-Orchestration Layer</h2>
|
||||
<p>The original decision left "what NetKingdom does about orchestration" defined only negatively (it does not own deployment mechanics). This refinement names the positive role. It sharpens, and does not overturn, the accepted decision: execution stays in Railiance.</p>
|
||||
<p>There are two distinct layers:</p>
|
||||
<ul><li><strong>Railiance — execution orchestration (the "how").</strong> A library of scenario playbooks plus the tools that run them. Each playbook provisions a slice of the landscape, ships <strong>sensible defaults</strong>, and <strong>exposes parameters</strong> for tuning. Multiple playbooks exist for multiple scenarios.</li><li><strong>NetKingdom — meta-orchestration (the "what" and "who").</strong> NetKingdom does not re-implement deployment mechanics. It (1) <strong>selects</strong> the services and playbooks a given scenario requires, (2) <strong>decides parametrization</strong> where tuning is warranted and otherwise relies on playbook defaults, and (3) <strong>holds the responsibility map</strong> — which element (a Railiance layer, an external provider, a tenant-owned piece) owns what across the whole IT landscape.</li></ul>
|
||||
<p>The relationship is architect ↔ contractor (or conductor ↔ players): NetKingdom composes the score and assigns parts; Railiance executes them. This is the same discipline used elsewhere — the IAM Profile is the contract while key-cape/Keycloak are implementations, and the State Hub is a read model over execution. Meta-orchestration is a <strong>decide/compose</strong> layer over Railiance's <strong>execute</strong> layer.</p>
|
||||
<p>This binds directly to the capability ladder (<code>docs/platform-identity-security-architecture.md</code> → <em>Capability Progression</em>): the ladder is the menu, a scenario selects a subset of tiers, and meta-orchestration is the act of binding that subset to playbooks + parameters + a responsibility map, producing a turn-key landscape.</p>
|
||||
<h3>New Dependency — Playbook / Capability Contract</h3>
|
||||
<p>For NetKingdom to select and parametrize reliably, <strong>Railiance playbooks must publish a declared interface</strong>: the capability each playbook provisions, its parameters (with defaults and constraints), and the responsibility it claims. This catalog is the orchestration-layer analog of the IAM Profile. Without it, meta-orchestration composes against implicit behavior and breaks when a playbook changes. Establishing this contract is the prerequisite for any concrete meta-orchestration work.</p>
|
||||
<h3>Effect on the Future Repo Trigger</h3>
|
||||
<p>Meta-orchestration logic now has a clear home (NetKingdom) regardless of whether a dedicated <strong>execution</strong>-orchestration repo is later created under the Future Repo Trigger above. A future repo, if created, would host reusable execution sequencing — not the scenario-composition and responsibility-mapping role, which remains NetKingdom's.</p>
|
||||
</section><footer><span>NK-ADR-0007 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0007-security-orchestration-boundary.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="b4fcff8448f07aca1fcb6908618bdb19f6c0e7dce25d4c5c8b85eec2f175233b">
|
||||
<title>Security Orchestration Boundary</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0007</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Security Orchestration Boundary</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0007-security-orchestration-boundary.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#future-repo-trigger"><span class="n">·</span>Future Repo Trigger</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#refinement-2026-05-21-meta-orchestration-layer"><span class="n">·</span>Refinement (2026-05-21): Meta-Orchestration Layer</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-05-18 <strong>Refined:</strong> 2026-05-21 (meta-orchestration layer — see below) <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The recursive platform security architecture needs careful sequencing: host trust, cluster trust, bootstrap secrets, runtime secret authority, runtime identity, runtime authorization, tenant onboarding, and readiness verification.</p>
|
||||
<p>That sequencing crosses NetKingdom and Railiance ownership boundaries. NetKingdom owns the canonical security architecture, IAM Profile, credential/bootstrap standards, and authorization semantics. Railiance owns deployment layering for infrastructure, clusters, platform services, and applications. OpenBao adds an important runtime-secret authority to the platform control plane, but it does not change those ownership boundaries.</p>
|
||||
<p>Creating a dedicated orchestration repo too early would risk encoding temporary bootstrap order and accidental stack assumptions as a permanent interface. Leaving every sequence implicit would also be risky: platform root actions, OpenBao initialization, policy import, and tenant onboarding must be auditable and repeatable.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Security orchestration will stay in Railiance playbooks for now.</p>
|
||||
<p>NetKingdom will define the trust-state model, readiness checks, policy semantics, OpenBao boundaries, and tenant/control-plane rules. Railiance playbooks will own the concrete deployment sequencing across <code>railiance-infra</code>, <code>railiance-cluster</code>, <code>railiance-platform</code>, and <code>railiance-apps</code>.</p>
|
||||
<p>A dedicated orchestration repo is deferred until the sequencing surface is stable enough to justify its own product boundary. If created later, it must coordinate safe sequencing and readiness reporting; it must not own security policy semantics or bypass Railiance stack ownership.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>NK-WP-0006 is implemented as architecture, standards, ADRs, and workplan constraints rather than a new repo.</li><li>OpenBao bootstrap, unseal/recovery, audit, backup, and workload-secret delivery belong in Railiance platform playbooks, governed by NetKingdom standards.</li><li>Cross-repo readiness should be reported as checks against explicit trust states, not as a hidden imperative script.</li><li>A future orchestration repo needs a new ADR before creation.</li></ul>
|
||||
</section>
|
||||
<section id="future-repo-trigger"><h2>Future Repo Trigger</h2>
|
||||
<p>Revisit a dedicated orchestration repo only if at least two of these are true:</p>
|
||||
<ul><li>multiple Railiance deployments need the same security sequencing interface;</li><li>readiness reporting becomes a reusable artifact consumed by operators, agents, or CI;</li><li>rollback and recovery workflows need a cross-repo state machine that no single Railiance layer can own cleanly;</li><li>tenant onboarding becomes a repeatable workflow spanning identity, flex-auth, Topaz, OpenBao, object storage, and application repos.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Create A Dedicated Orchestration Repo Now</h3>
|
||||
<p>This would give sequencing a visible home, but it would probably encode unstable details before OpenBao runtime operations, flex-auth/Topaz policy import, and tenant onboarding have enough implementation feedback.</p>
|
||||
<h3>Put Orchestration In NetKingdom</h3>
|
||||
<p>NetKingdom owns the security model, but it should not become the deployment repo for every stack layer. This would blur architecture ownership with platform deployment ownership.</p>
|
||||
<h3>Leave Sequencing Entirely Informal</h3>
|
||||
<p>This avoids premature structure but leaves bootstrap and runtime trust transitions too dependent on operator memory. The accepted approach keeps the sequence explicit while leaving concrete deployment in the Railiance stack.</p>
|
||||
</section>
|
||||
<section id="refinement-2026-05-21-meta-orchestration-layer"><h2>Refinement (2026-05-21): Meta-Orchestration Layer</h2>
|
||||
<p>The original decision left "what NetKingdom does about orchestration" defined only negatively (it does not own deployment mechanics). This refinement names the positive role. It sharpens, and does not overturn, the accepted decision: execution stays in Railiance.</p>
|
||||
<p>There are two distinct layers:</p>
|
||||
<ul><li><strong>Railiance — execution orchestration (the "how").</strong> A library of scenario playbooks plus the tools that run them. Each playbook provisions a slice of the landscape, ships <strong>sensible defaults</strong>, and <strong>exposes parameters</strong> for tuning. Multiple playbooks exist for multiple scenarios.</li><li><strong>NetKingdom — meta-orchestration (the "what" and "who").</strong> NetKingdom does not re-implement deployment mechanics. It (1) <strong>selects</strong> the services and playbooks a given scenario requires, (2) <strong>decides parametrization</strong> where tuning is warranted and otherwise relies on playbook defaults, and (3) <strong>holds the responsibility map</strong> — which element (a Railiance layer, an external provider, a tenant-owned piece) owns what across the whole IT landscape.</li></ul>
|
||||
<p>The relationship is architect ↔ contractor (or conductor ↔ players): NetKingdom composes the score and assigns parts; Railiance executes them. This is the same discipline used elsewhere — the IAM Profile is the contract while key-cape/Keycloak are implementations, and the State Hub is a read model over execution. Meta-orchestration is a <strong>decide/compose</strong> layer over Railiance's <strong>execute</strong> layer.</p>
|
||||
<p>This binds directly to the capability ladder (<code>docs/platform-identity-security-architecture.md</code> → <em>Capability Progression</em>): the ladder is the menu, a scenario selects a subset of tiers, and meta-orchestration is the act of binding that subset to playbooks + parameters + a responsibility map, producing a turn-key landscape.</p>
|
||||
<h3>New Dependency — Playbook / Capability Contract</h3>
|
||||
<p>For NetKingdom to select and parametrize reliably, <strong>Railiance playbooks must publish a declared interface</strong>: the capability each playbook provisions, its parameters (with defaults and constraints), and the responsibility it claims. This catalog is the orchestration-layer analog of the IAM Profile. Without it, meta-orchestration composes against implicit behavior and breaks when a playbook changes. Establishing this contract is the prerequisite for any concrete meta-orchestration work.</p>
|
||||
<h3>Effect on the Future Repo Trigger</h3>
|
||||
<p>Meta-orchestration logic now has a clear home (NetKingdom) regardless of whether a dedicated <strong>execution</strong>-orchestration repo is later created under the Future Repo Trigger above. A future repo, if created, would host reusable execution sequencing — not the scenario-composition and responsibility-mapping role, which remains NetKingdom's.</p>
|
||||
</section><footer><span>NK-ADR-0007 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0007-security-orchestration-boundary.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
225
build/adr/netkingdom-tenant-capability-ownership/v1/index.html
Normal file
225
build/adr/netkingdom-tenant-capability-ownership/v1/index.html
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="843f7a65f0fc145d08a73e364bc9a1dee0f7b8af934abf1584ee39dffa903ee0">
|
||||
<title>Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0014</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-07-23 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0013 introduced the tenant onboarding grouping taxonomy (<code>trial</code>/<code>friendly</code>/<code>single</code>/.../<code>agentic</code>), deliberately orthogonal to a separate, unratified <strong>capability-role</strong> model sketched in <code>docs/princedom-isolation-exploration.md</code>: <code>PLTF</code> (operates the platform), <code>IAM</code> (organizes its own users/auth/secrets), <code>VEN</code> (provides apps/services to others), <code>CUS</code> (consumes apps/services from <code>PLTF</code> or <code>VEN</code> tenants) — non-exclusive, a tenant may hold several at once.</p>
|
||||
<p>That exploration left open where capability roles actually live (a per-token claim vs. a registry), who owns them, how they're granted or revoked, and how this interacts with the IAM Profile's existing <code>roles</code> claim — which is a <em>per-subject</em> claim ("coarse identity roles" for the human/service/agent holding the token), a different concept from a <em>per-tenant</em> capability fact. Conflating the two would be a category error: <code>roles: ["VEN"]</code> on a token would ambiguously mean "this subject has vendor-role" vs. "this subject's tenant is a vendor."</p>
|
||||
<p>No existing service owns tenant-as-an-entity facts (existence, grouping, capability roles, plan/subscription state) as a queryable resource. <code>user-engine</code>'s own boundary contract (<code>canon/standards/user-engine-boundary-contract_v0.1.md</code>) explicitly scopes <code>user-engine</code> to <em>consuming</em> tenant identifiers and <em>storing tenant-scoped records</em>, not owning tenant identity or capability facts.</p>
|
||||
<p>Bernd's direction (2026-07-23):</p>
|
||||
<ul><li>Implement the previously-discussed hybrid carrying approach: cache a tenant's capability roles on the token for ordinary decisions, but require a live check for critical/high-stakes actions.</li><li>Role grants are usually tied to a payment plan — most concretely, <code>IAM</code> means the tenant has its own dedicated key-cape/Keycloak instance for isolation, scale, and performance, which is itself a paid capability.</li><li><code>trial</code>-grouped tenants may hold <strong>any</strong> capability role without restriction, specifically so the platform can showcase, test, and explore every role. Safety for trial tenants comes from resource guardrails (spend limits defaulting to zero budget, entity/action count limits), not from role gating — guardrail design itself is future work, not this ADR.</li><li>A new service, <code>tenant-engine</code>, will be built (Bernd) as the owner of this domain, organized beside <code>user-engine</code> rather than inside it — smaller, single-purpose services are easier to reason about and drift less, matching the fleet's existing convention (<code>activity-core</code>, <code>audit-core</code>, <code>user-engine</code>, and others).</li></ul>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<ol><li><strong>Capability-role vocabulary ratified as core NetKingdom vocabulary</strong>: <code>PLTF</code>, <code>IAM</code>, <code>VEN</code>, <code>CUS</code>, non-exclusive. <code>IAM</code> specifically means: the tenant operates its own dedicated IAM implementation instance (lightweight key-cape or expanded Keycloak) rather than sharing the platform's, for isolation/scale/performance — not "any tenant that happens to have users."</li></ol>
|
||||
<ol><li><strong><code>tenant-engine</code> is the canonical owner</strong> of tenant-domain facts: tenant existence, grouping (ADR-0013), capability roles (this ADR), plan/ subscription assignment, and — reserved for future design, not built now — guardrail/quota policy. It is a new, separate service, not a module inside <code>user-engine</code>. Its ownership boundary is defined in the companion contract, <code>canon/standards/tenant-engine-boundary-contract_v0.1.md</code>.</li></ol>
|
||||
<ol><li><strong>Carrying mechanism: hybrid cache + live re-validation.</strong> <code>tenant-engine</code> is the single source of truth. <code>key-cape</code> stamps a cached, optional <code>tenant_roles</code> claim onto issued tokens at issuance time, sourced from <code>tenant-engine</code> (added to the IAM Profile as a new optional claim — <code>canon/standards/iam-profile_v0.3.md</code>). Consumers may trust the cached claim for ordinary decisions. <code>flex-auth</code> <strong>MUST</strong> re-validate live against <code>tenant-engine</code> — never trust the cached claim alone — before authorizing privileged or high-stakes actions, using the same threshold class the profile already defines for <code>assurance.level >= aal2</code> (privileged, destructive, platform-root, secret, credential-vending flows). This bounds staleness risk for ordinary actions to a token's short lifetime (5–30 minutes for service/agent tokens, per the profile's Token Lifecycle table) while guaranteeing freshness exactly where it matters most.</li></ol>
|
||||
<ol><li><strong>Role governance is plan-linked.</strong> Granting a role is normally a consequence of a tenant's payment-plan state in <code>tenant-engine</code>, not a separate manual workflow — starting with <code>IAM</code>. <code>tenant-engine</code> records which plan grants which role(s); <code>adaptive-pricing</code> remains the source of plan/pricing-model <em>definitions</em>, <code>tenant-engine</code> owns the tenant's <em>current</em> plan/subscription assignment, referenced by id, never duplicated locally. Whether <code>VEN</code> needs an approval gate beyond payment (reselling access carries legal/compliance exposure a payment alone doesn't cover) is <strong>not resolved by this ADR</strong> — left to <code>tenant-engine</code>'s own workplan.</li></ol>
|
||||
<ol><li><strong>Trial tenants may hold any capability role, unrestricted.</strong> The <code>trial</code> grouping's purpose (showcase, test, explore) requires demonstrating every role. Safety is enforced through resource guardrails instead: trial tenants default to a <strong>spend budget of zero</strong>, with entity and action count limits to follow. Guardrail policy design (exact limits, enforcement point, override process) is real, near-term future work, reserved as <code>tenant-engine</code>'s to own once designed — not specified by this ADR.</li></ol>
|
||||
<ol><li><strong>Grouping and capability role are independent axes</strong> recorded on the same tenant record in <code>tenant-engine</code>. Neither constrains the other except where a future guardrail policy explicitly says so.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>canon/standards/iam-profile_v0.3.md</code> adds the optional <code>tenant_roles</code> claim and folds in ADR-0013's tenant-identifier vocabulary update (both non-breaking per ADR-0011's own minor-version rule — no existing implementation is invalidated by either change). Supersedes <code>iam-profile_v0.2.md</code>.</li><li><code>tenant-engine</code> becomes a new repository with its own workplans (Bernd). <code>canon/standards/tenant-engine-boundary-contract_v0.1.md</code> defines its ownership boundary now, before code exists — the same sequencing <code>user-engine</code>'s contract followed.</li><li><code>flex-auth</code> policy packages gating high-stakes actions must add a <code>tenant-engine</code> live-lookup step; they cannot trust <code>tenant_roles</code> alone for those decisions.</li><li><code>key-cape</code> needs a <code>tenant-engine</code> integration at token-issuance time to source the cached claim — tracked in <code>key-cape</code>'s own workplans, not here.</li><li><code>docs/platform-identity-security-architecture.md</code>'s Tenant Model section is updated to reflect the grouping + role split and <code>tenant-engine</code>'s role (companion change alongside this ADR).</li><li>Guardrail/quota policy is named as required near-term work and given an owner (<code>tenant-engine</code>), but is explicitly not designed by this ADR.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Token-claim-only, no live re-validation</h3>
|
||||
<p>Rejected: staleness would be unbounded within a token's lifetime for genuinely high-stakes actions. A stale <code>VEN</code> grant surviving a plan downgrade or cancellation is not an acceptable risk for money-movement or credential-vending flows — exactly the class the profile already treats as requiring the strongest assurance.</p>
|
||||
<h3>Registry-only, no cached claim</h3>
|
||||
<p>Rejected: every ordinary request would pay a <code>tenant-engine</code> round-trip even for non-critical checks, adding latency and a hard runtime dependency for every consumer, not just the ones handling privileged actions.</p>
|
||||
<h3>Restrict role eligibility by tenant grouping (e.g., <code>trial</code> cannot hold <code>VEN</code>)</h3>
|
||||
<p>Rejected per Bernd's direction: <code>trial</code> tenants exist specifically to showcase every role. Resource guardrails are the intended safety mechanism instead, keeping the two axes (grouping, role) independent.</p>
|
||||
<h3>Put tenant-role/plan storage inside <code>user-engine</code></h3>
|
||||
<p>Rejected. <code>user-engine</code>'s own boundary contract scopes it to consuming tenant identifiers and storing tenant-<em>scoped</em> user records, not owning tenant-as-an-entity facts. A dedicated service avoids coupling a security-critical, high-frequency lookup (used by <code>flex-auth</code> on every privileged decision, and by <code>key-cape</code> on every token issuance) to <code>user-engine</code>'s much larger surface (registration flows, factor models, family dataspace onboarding) that has nothing to do with tenant capability state.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li><code>tenant-engine</code> repository creation and its own workplan (Bernd).</li><li><code>key-cape</code> integration: source <code>tenant_roles</code> from <code>tenant-engine</code> at token issuance.</li><li><code>flex-auth</code> policy package updates: live <code>tenant-engine</code> re-validation gate for privileged actions.</li><li>Guardrail/quota policy design for <code>trial</code> (and eventually all) tenants: spend limits, entity/action count limits, enforcement point, override process.</li><li>Resolve whether <code>VEN</code> needs an approval gate beyond payment-plan state.</li></ul>
|
||||
</section><footer><span>NK-ADR-0014 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="843f7a65f0fc145d08a73e364bc9a1dee0f7b8af934abf1584ee39dffa903ee0">
|
||||
<title>Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0014</span> <span class="stat">accepted · 1</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-07-23 <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0013 introduced the tenant onboarding grouping taxonomy (<code>trial</code>/<code>friendly</code>/<code>single</code>/.../<code>agentic</code>), deliberately orthogonal to a separate, unratified <strong>capability-role</strong> model sketched in <code>docs/princedom-isolation-exploration.md</code>: <code>PLTF</code> (operates the platform), <code>IAM</code> (organizes its own users/auth/secrets), <code>VEN</code> (provides apps/services to others), <code>CUS</code> (consumes apps/services from <code>PLTF</code> or <code>VEN</code> tenants) — non-exclusive, a tenant may hold several at once.</p>
|
||||
<p>That exploration left open where capability roles actually live (a per-token claim vs. a registry), who owns them, how they're granted or revoked, and how this interacts with the IAM Profile's existing <code>roles</code> claim — which is a <em>per-subject</em> claim ("coarse identity roles" for the human/service/agent holding the token), a different concept from a <em>per-tenant</em> capability fact. Conflating the two would be a category error: <code>roles: ["VEN"]</code> on a token would ambiguously mean "this subject has vendor-role" vs. "this subject's tenant is a vendor."</p>
|
||||
<p>No existing service owns tenant-as-an-entity facts (existence, grouping, capability roles, plan/subscription state) as a queryable resource. <code>user-engine</code>'s own boundary contract (<code>canon/standards/user-engine-boundary-contract_v0.1.md</code>) explicitly scopes <code>user-engine</code> to <em>consuming</em> tenant identifiers and <em>storing tenant-scoped records</em>, not owning tenant identity or capability facts.</p>
|
||||
<p>Bernd's direction (2026-07-23):</p>
|
||||
<ul><li>Implement the previously-discussed hybrid carrying approach: cache a tenant's capability roles on the token for ordinary decisions, but require a live check for critical/high-stakes actions.</li><li>Role grants are usually tied to a payment plan — most concretely, <code>IAM</code> means the tenant has its own dedicated key-cape/Keycloak instance for isolation, scale, and performance, which is itself a paid capability.</li><li><code>trial</code>-grouped tenants may hold <strong>any</strong> capability role without restriction, specifically so the platform can showcase, test, and explore every role. Safety for trial tenants comes from resource guardrails (spend limits defaulting to zero budget, entity/action count limits), not from role gating — guardrail design itself is future work, not this ADR.</li><li>A new service, <code>tenant-engine</code>, will be built (Bernd) as the owner of this domain, organized beside <code>user-engine</code> rather than inside it — smaller, single-purpose services are easier to reason about and drift less, matching the fleet's existing convention (<code>activity-core</code>, <code>audit-core</code>, <code>user-engine</code>, and others).</li></ul>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<ol><li><strong>Capability-role vocabulary ratified as core NetKingdom vocabulary</strong>: <code>PLTF</code>, <code>IAM</code>, <code>VEN</code>, <code>CUS</code>, non-exclusive. <code>IAM</code> specifically means: the tenant operates its own dedicated IAM implementation instance (lightweight key-cape or expanded Keycloak) rather than sharing the platform's, for isolation/scale/performance — not "any tenant that happens to have users."</li></ol>
|
||||
<ol><li><strong><code>tenant-engine</code> is the canonical owner</strong> of tenant-domain facts: tenant existence, grouping (ADR-0013), capability roles (this ADR), plan/ subscription assignment, and — reserved for future design, not built now — guardrail/quota policy. It is a new, separate service, not a module inside <code>user-engine</code>. Its ownership boundary is defined in the companion contract, <code>canon/standards/tenant-engine-boundary-contract_v0.1.md</code>.</li></ol>
|
||||
<ol><li><strong>Carrying mechanism: hybrid cache + live re-validation.</strong> <code>tenant-engine</code> is the single source of truth. <code>key-cape</code> stamps a cached, optional <code>tenant_roles</code> claim onto issued tokens at issuance time, sourced from <code>tenant-engine</code> (added to the IAM Profile as a new optional claim — <code>canon/standards/iam-profile_v0.3.md</code>). Consumers may trust the cached claim for ordinary decisions. <code>flex-auth</code> <strong>MUST</strong> re-validate live against <code>tenant-engine</code> — never trust the cached claim alone — before authorizing privileged or high-stakes actions, using the same threshold class the profile already defines for <code>assurance.level >= aal2</code> (privileged, destructive, platform-root, secret, credential-vending flows). This bounds staleness risk for ordinary actions to a token's short lifetime (5–30 minutes for service/agent tokens, per the profile's Token Lifecycle table) while guaranteeing freshness exactly where it matters most.</li></ol>
|
||||
<ol><li><strong>Role governance is plan-linked.</strong> Granting a role is normally a consequence of a tenant's payment-plan state in <code>tenant-engine</code>, not a separate manual workflow — starting with <code>IAM</code>. <code>tenant-engine</code> records which plan grants which role(s); <code>adaptive-pricing</code> remains the source of plan/pricing-model <em>definitions</em>, <code>tenant-engine</code> owns the tenant's <em>current</em> plan/subscription assignment, referenced by id, never duplicated locally. Whether <code>VEN</code> needs an approval gate beyond payment (reselling access carries legal/compliance exposure a payment alone doesn't cover) is <strong>not resolved by this ADR</strong> — left to <code>tenant-engine</code>'s own workplan.</li></ol>
|
||||
<ol><li><strong>Trial tenants may hold any capability role, unrestricted.</strong> The <code>trial</code> grouping's purpose (showcase, test, explore) requires demonstrating every role. Safety is enforced through resource guardrails instead: trial tenants default to a <strong>spend budget of zero</strong>, with entity and action count limits to follow. Guardrail policy design (exact limits, enforcement point, override process) is real, near-term future work, reserved as <code>tenant-engine</code>'s to own once designed — not specified by this ADR.</li></ol>
|
||||
<ol><li><strong>Grouping and capability role are independent axes</strong> recorded on the same tenant record in <code>tenant-engine</code>. Neither constrains the other except where a future guardrail policy explicitly says so.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>canon/standards/iam-profile_v0.3.md</code> adds the optional <code>tenant_roles</code> claim and folds in ADR-0013's tenant-identifier vocabulary update (both non-breaking per ADR-0011's own minor-version rule — no existing implementation is invalidated by either change). Supersedes <code>iam-profile_v0.2.md</code>.</li><li><code>tenant-engine</code> becomes a new repository with its own workplans (Bernd). <code>canon/standards/tenant-engine-boundary-contract_v0.1.md</code> defines its ownership boundary now, before code exists — the same sequencing <code>user-engine</code>'s contract followed.</li><li><code>flex-auth</code> policy packages gating high-stakes actions must add a <code>tenant-engine</code> live-lookup step; they cannot trust <code>tenant_roles</code> alone for those decisions.</li><li><code>key-cape</code> needs a <code>tenant-engine</code> integration at token-issuance time to source the cached claim — tracked in <code>key-cape</code>'s own workplans, not here.</li><li><code>docs/platform-identity-security-architecture.md</code>'s Tenant Model section is updated to reflect the grouping + role split and <code>tenant-engine</code>'s role (companion change alongside this ADR).</li><li>Guardrail/quota policy is named as required near-term work and given an owner (<code>tenant-engine</code>), but is explicitly not designed by this ADR.</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Token-claim-only, no live re-validation</h3>
|
||||
<p>Rejected: staleness would be unbounded within a token's lifetime for genuinely high-stakes actions. A stale <code>VEN</code> grant surviving a plan downgrade or cancellation is not an acceptable risk for money-movement or credential-vending flows — exactly the class the profile already treats as requiring the strongest assurance.</p>
|
||||
<h3>Registry-only, no cached claim</h3>
|
||||
<p>Rejected: every ordinary request would pay a <code>tenant-engine</code> round-trip even for non-critical checks, adding latency and a hard runtime dependency for every consumer, not just the ones handling privileged actions.</p>
|
||||
<h3>Restrict role eligibility by tenant grouping (e.g., <code>trial</code> cannot hold <code>VEN</code>)</h3>
|
||||
<p>Rejected per Bernd's direction: <code>trial</code> tenants exist specifically to showcase every role. Resource guardrails are the intended safety mechanism instead, keeping the two axes (grouping, role) independent.</p>
|
||||
<h3>Put tenant-role/plan storage inside <code>user-engine</code></h3>
|
||||
<p>Rejected. <code>user-engine</code>'s own boundary contract scopes it to consuming tenant identifiers and storing tenant-<em>scoped</em> user records, not owning tenant-as-an-entity facts. A dedicated service avoids coupling a security-critical, high-frequency lookup (used by <code>flex-auth</code> on every privileged decision, and by <code>key-cape</code> on every token issuance) to <code>user-engine</code>'s much larger surface (registration flows, factor models, family dataspace onboarding) that has nothing to do with tenant capability state.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li><code>tenant-engine</code> repository creation and its own workplan (Bernd).</li><li><code>key-cape</code> integration: source <code>tenant_roles</code> from <code>tenant-engine</code> at token issuance.</li><li><code>flex-auth</code> policy package updates: live <code>tenant-engine</code> re-validation gate for privileged actions.</li><li>Guardrail/quota policy design for <code>trial</code> (and eventually all) tenants: spend limits, entity/action count limits, enforcement point, override process.</li><li>Resolve whether <code>VEN</code> needs an approval gate beyond payment-plan state.</li></ul>
|
||||
</section><footer><span>NK-ADR-0014 · 1 · accepted</span><span>net-kingdom · docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
241
build/adr/netkingdom-tenant-onboarding-taxonomy/v1/index.html
Normal file
241
build/adr/netkingdom-tenant-onboarding-taxonomy/v1/index.html
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="d4e57e63126d2cca1d381c025170e4b1f678c3f3">
|
||||
<meta name="policy-source-digest" content="3a6030a8958176a902942ffd29154104ba3441a09d06edf3deaeadc7291ee0d4">
|
||||
<title>Tenant Onboarding Grouping Taxonomy</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0013</span> <span class="stat">accepted · 2</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Tenant Onboarding Grouping Taxonomy</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#scope-and-governance-classification"><span class="n">·</span>Scope and Governance Classification</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-07-23 <strong>Amended:</strong> 2026-08-22 (current classification versus historical identifier segment) <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>canon/standards/iam-profile_v0.2.md</code>'s "Tenant Claim" section lists four <em>suggested</em> (not exhaustive) tenant identifiers: <code>tenant:platform</code>, <code>tenant:coulomb</code>, <code>tenant:sandbox:<name></code>, <code>tenant:customer:<name></code>.</p>
|
||||
<p>Separately, an unratified exploration (<code>docs/princedom-isolation-exploration.md</code>) proposes a non-exclusive <strong>capability-role</strong> model for tenants: <code>PLTF</code> (operates the platform), <code>IAM</code> (organizes its own users/secrets), <code>VEN</code> (provides apps/services to others), <code>CUS</code> (consumes apps/services from <code>PLTF</code> or <code>VEN</code> tenants) — one tenant can hold multiple roles simultaneously.</p>
|
||||
<p>Binky Hedgehog GmbH is being onboarded as the platform's first tenant outside <code>tenant:coulomb</code> (<code>key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md</code>), forcing a concrete identifier decision. Binky already exercises both <code>CUS</code> (consumes <code>qonto-assistant</code>) and, per <code>binky-control/INTENT.md</code>, will exercise <code>VEN</code> (packages and sells offers built on the ecosystem to real external customers) — it is not purely a "customer" in the role sense.</p>
|
||||
<p>Using a capability-role word (<code>customer</code>, <code>vendor</code>, ...) as the tenant <em>grouping</em> segment collides with that separate role dimension: a tenant labeled <code>tenant:customer:binky</code> would carry a stale, misleading label the moment it also starts acting as a vendor, and tenant identifiers are expensive to rename once tokens, OpenBao paths, and downstream config reference them.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Adopt an onboarding-risk / entity-shape <strong>grouping</strong> taxonomy for the tenant identifier's second segment, replacing the <code>sandbox</code>/<code>customer</code> suggested identifiers in <code>iam-profile_v0.2.md</code>'s "Tenant Claim" section:</p>
|
||||
<pre>tenant:<grouping>:<name>
|
||||
|
||||
trial - test/trial/showcase tenants only
|
||||
friendly - known, easily reached, tolerant of experimentation/instability
|
||||
single - one-person business entities (freelance consultants)
|
||||
small - up to 10 employees at time of onboarding (attoo)
|
||||
medium - up to 100 employees (attoo)
|
||||
large - up to 1000 employees (attoo)
|
||||
enterprise - 1001+ employees (attoo)
|
||||
consumer - private individuals
|
||||
family - a legal family
|
||||
community - a non-legal group of people
|
||||
association - a legal association of people
|
||||
agentic - financially enabled AI entities</pre>
|
||||
<p>The taxonomy has two deliberately different uses:</p>
|
||||
<ul><li>At creation, the identifier's grouping segment records the tenant's onboarding-time classification. The complete identifier is immutable, so this segment is historical after creation.</li><li>The tenant record's <code>grouping</code> field records current classification. It may change as the entity changes and is authoritative for present-day policy, including guardrails and spend ceilings.</li></ul>
|
||||
<p>No consumer may parse the identifier's middle segment and treat it as current grouping. Consumers needing current grouping MUST read it from <code>tenant-engine</code>. Identifier creation still validates the segment against this vocabulary; historical does not mean free-form or optional.</p>
|
||||
<p>Grouping is deliberately <strong>orthogonal to capability role</strong> (<code>PLTF</code>/<code>IAM</code>/<code>VEN</code>/<code>CUS</code>, subsequently ratified by ADR-0014): grouping describes <em>what kind of entity a tenant is and its current onboarding-risk classification</em>; role describes <em>what it does on the platform</em>. Both are carried as tenant metadata, but never conflated into the immutable identifier segment — that conflation is exactly what this ADR avoids.</p>
|
||||
<p><code>tenant:platform</code> and <code>tenant:coulomb</code> remain <strong>reserved, ungrouped identifiers outside this taxonomy</strong>: <code>tenant:platform</code> is the control-plane root, not a business entity being onboarded; <code>tenant:coulomb</code> is the reference tenant established by ADR-0006, predating this taxonomy, and none of the twelve groupings meaningfully describe "the platform building itself." The taxonomy applies to tenants onboarded from here forward. <em>(This sub-point completes an open question raised during KEY-WP-0004 drafting and is Codex's reasoned proposal — flagged for Bernd's explicit confirmation rather than assumed settled.)</em></p>
|
||||
<p>First application: Binky Hedgehog GmbH maps to <code>friendly</code> — known, reachable, tolerant of early instability — giving <strong><code>tenant:friendly:binky</code></strong> (<code>key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md</code>).</p>
|
||||
</section>
|
||||
<section id="scope-and-governance-classification"><h2>Scope and Governance Classification</h2>
|
||||
<p>Per ADR-0011's breaking-change governance, this does <strong>not</strong> meet the breaking-change bar: it doesn't add, remove, or rename a required claim; it doesn't change <code>tenant</code>'s type or validation rule (still an opaque string); it doesn't affect acceptance of any previously-issued token (<code>tenant:platform</code>, <code>tenant:coulomb</code> tokens remain valid as-is, and no <code>tenant:customer:*</code> or <code>tenant:sandbox:*</code> tokens have been issued yet). It only replaces non-normative suggested-identifier guidance for tenants onboarded going forward.</p>
|
||||
<p>Per ADR-0011's own versioning rule ("New versions are added as new files... except for clearly editorial fixes that do not affect semantics"), this qualifies as an <strong>editorial update</strong> to <code>iam-profile_v0.2.md</code>'s Tenant Claim section — not a new versioned profile document.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>canon/standards/iam-profile_v0.2.md</code>'s "Tenant Claim" section needs a follow-up edit replacing the <code>sandbox</code>/<code>customer</code> suggested identifiers with this taxonomy. Not made by this ADR itself — tracked as follow-up so the change is reviewable on its own.</li><li><code>key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md</code> already reflects this decision (<code>tenant:friendly:binky</code>).</li><li>Future tenant onboarding work should classify a tenant against this list before minting an identifier, rather than reaching for a role word.</li><li>Tenant identifiers never change when current grouping changes. The middle segment is creation-time history; <code>tenant-engine</code> is authoritative for the current grouping value.</li><li>Policy and commercial consumers, including spend-ceiling resolution, MUST query <code>tenant-engine</code> and MUST NOT derive current grouping by splitting a tenant identifier.</li><li>The capability-role model (<code>PLTF</code>/<code>IAM</code>/<code>VEN</code>/<code>CUS</code>) remains a separate dimension, now ratified by ADR-0014. Role metadata is carried alongside — not instead of — current grouping and the historical identifier segment.</li><li><code>tenant:platform</code> and <code>tenant:coulomb</code> are reserved outside the taxonomy, pending Bernd's explicit confirmation (see Decision).</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Keep using capability-role words as the tenant grouping</h3>
|
||||
<p>Rejected: tenants are already known to hold multiple roles at once (Binky is <code>CUS</code> now, <code>VEN</code> later); a role-word grouping would need renaming as a tenant's roles evolve, which tenant identifiers are specifically expensive to do once tokens, OpenBao paths, and config reference them.</p>
|
||||
<h3>Retrofit <code>tenant:coulomb</code> into the new taxonomy</h3>
|
||||
<p>Considered <code>tenant:friendly:coulomb</code> or similar. Rejected for now: Coulomb is structurally the reference tenant from ADR-0006, predating this taxonomy, and none of the twelve entity-shape groupings describe "the platform's own ecosystem-development tenant." Revisit if a future grouping is ever added that genuinely fits it.</p>
|
||||
<h3>Do nothing / keep the four original suggested identifiers</h3>
|
||||
<p>Rejected: <code>tenant:customer:binky</code> was the working default going into Binky's onboarding despite the role-collision problem above; the fleet needs this resolved before the first non-Coulomb tenant goes live, not after.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li>Edit <code>canon/standards/iam-profile_v0.2.md</code>'s Tenant Claim section to replace the old suggested identifiers with this taxonomy (separate, reviewable change).</li><li>Confirm the <code>tenant:platform</code>/<code>tenant:coulomb</code> reserved/ungrouped treatment explicitly.</li><li>ADR-0014 and the Tenant Engine Boundary Contract define how capability-role metadata is carried alongside grouping.</li><li>Keep <code>tenant-engine</code>'s identifier parser vocabulary-validating for creation and lookup compatibility, but do not expose parsed grouping as current classification.</li></ul>
|
||||
</section><footer><span>NK-ADR-0013 · 2 · accepted</span><span>net-kingdom · docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md · d4e57e63126d2cca1d381c025170e4b1f678c3f3</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="ce198fc2905687ea90a2892346b6860281ac87f8">
|
||||
<meta name="policy-source-digest" content="3a6030a8958176a902942ffd29154104ba3441a09d06edf3deaeadc7291ee0d4">
|
||||
<title>Tenant Onboarding Grouping Taxonomy</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>NK-ADR-0013</span> <span class="stat">accepted · 2</span> <span>net-kingdom</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>Tenant Onboarding Grouping Taxonomy</h1><p class="sub">Source: <code>net-kingdom · docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md · ce198fc2905687ea90a2892346b6860281ac87f8</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#scope-and-governance-classification"><span class="n">·</span>Scope and Governance Classification</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives Considered</a></li><li><a href="#follow-up"><span class="n">·</span>Follow-Up</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-07-23 <strong>Amended:</strong> 2026-08-22 (current classification versus historical identifier segment) <strong>Deciders:</strong> Bernd Worsch, Codex</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>canon/standards/iam-profile_v0.2.md</code>'s "Tenant Claim" section lists four <em>suggested</em> (not exhaustive) tenant identifiers: <code>tenant:platform</code>, <code>tenant:coulomb</code>, <code>tenant:sandbox:<name></code>, <code>tenant:customer:<name></code>.</p>
|
||||
<p>Separately, an unratified exploration (<code>docs/princedom-isolation-exploration.md</code>) proposes a non-exclusive <strong>capability-role</strong> model for tenants: <code>PLTF</code> (operates the platform), <code>IAM</code> (organizes its own users/secrets), <code>VEN</code> (provides apps/services to others), <code>CUS</code> (consumes apps/services from <code>PLTF</code> or <code>VEN</code> tenants) — one tenant can hold multiple roles simultaneously.</p>
|
||||
<p>Binky Hedgehog GmbH is being onboarded as the platform's first tenant outside <code>tenant:coulomb</code> (<code>key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md</code>), forcing a concrete identifier decision. Binky already exercises both <code>CUS</code> (consumes <code>qonto-assistant</code>) and, per <code>binky-control/INTENT.md</code>, will exercise <code>VEN</code> (packages and sells offers built on the ecosystem to real external customers) — it is not purely a "customer" in the role sense.</p>
|
||||
<p>Using a capability-role word (<code>customer</code>, <code>vendor</code>, ...) as the tenant <em>grouping</em> segment collides with that separate role dimension: a tenant labeled <code>tenant:customer:binky</code> would carry a stale, misleading label the moment it also starts acting as a vendor, and tenant identifiers are expensive to rename once tokens, OpenBao paths, and downstream config reference them.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Adopt an onboarding-risk / entity-shape <strong>grouping</strong> taxonomy for the tenant identifier's second segment, replacing the <code>sandbox</code>/<code>customer</code> suggested identifiers in <code>iam-profile_v0.2.md</code>'s "Tenant Claim" section:</p>
|
||||
<pre>tenant:<grouping>:<name>
|
||||
|
||||
trial - test/trial/showcase tenants only
|
||||
friendly - known, easily reached, tolerant of experimentation/instability
|
||||
single - one-person business entities (freelance consultants)
|
||||
small - up to 10 employees at time of onboarding (attoo)
|
||||
medium - up to 100 employees (attoo)
|
||||
large - up to 1000 employees (attoo)
|
||||
enterprise - 1001+ employees (attoo)
|
||||
consumer - private individuals
|
||||
family - a legal family
|
||||
community - a non-legal group of people
|
||||
association - a legal association of people
|
||||
agentic - financially enabled AI entities</pre>
|
||||
<p>The taxonomy has two deliberately different uses:</p>
|
||||
<ul><li>At creation, the identifier's grouping segment records the tenant's onboarding-time classification. The complete identifier is immutable, so this segment is historical after creation.</li><li>The tenant record's <code>grouping</code> field records current classification. It may change as the entity changes and is authoritative for present-day policy, including guardrails and spend ceilings.</li></ul>
|
||||
<p>No consumer may parse the identifier's middle segment and treat it as current grouping. Consumers needing current grouping MUST read it from <code>tenant-engine</code>. Identifier creation still validates the segment against this vocabulary; historical does not mean free-form or optional.</p>
|
||||
<p>Grouping is deliberately <strong>orthogonal to capability role</strong> (<code>PLTF</code>/<code>IAM</code>/<code>VEN</code>/<code>CUS</code>, subsequently ratified by ADR-0014): grouping describes <em>what kind of entity a tenant is and its current onboarding-risk classification</em>; role describes <em>what it does on the platform</em>. Both are carried as tenant metadata, but never conflated into the immutable identifier segment — that conflation is exactly what this ADR avoids.</p>
|
||||
<p><code>tenant:platform</code> and <code>tenant:coulomb</code> remain <strong>reserved, ungrouped identifiers outside this taxonomy</strong>: <code>tenant:platform</code> is the control-plane root, not a business entity being onboarded; <code>tenant:coulomb</code> is the reference tenant established by ADR-0006, predating this taxonomy, and none of the twelve groupings meaningfully describe "the platform building itself." The taxonomy applies to tenants onboarded from here forward. <em>(This sub-point completes an open question raised during KEY-WP-0004 drafting and is Codex's reasoned proposal — flagged for Bernd's explicit confirmation rather than assumed settled.)</em></p>
|
||||
<p>First application: Binky Hedgehog GmbH maps to <code>friendly</code> — known, reachable, tolerant of early instability — giving <strong><code>tenant:friendly:binky</code></strong> (<code>key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md</code>).</p>
|
||||
</section>
|
||||
<section id="scope-and-governance-classification"><h2>Scope and Governance Classification</h2>
|
||||
<p>Per ADR-0011's breaking-change governance, this does <strong>not</strong> meet the breaking-change bar: it doesn't add, remove, or rename a required claim; it doesn't change <code>tenant</code>'s type or validation rule (still an opaque string); it doesn't affect acceptance of any previously-issued token (<code>tenant:platform</code>, <code>tenant:coulomb</code> tokens remain valid as-is, and no <code>tenant:customer:*</code> or <code>tenant:sandbox:*</code> tokens have been issued yet). It only replaces non-normative suggested-identifier guidance for tenants onboarded going forward.</p>
|
||||
<p>Per ADR-0011's own versioning rule ("New versions are added as new files... except for clearly editorial fixes that do not affect semantics"), this qualifies as an <strong>editorial update</strong> to <code>iam-profile_v0.2.md</code>'s Tenant Claim section — not a new versioned profile document.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>canon/standards/iam-profile_v0.2.md</code>'s "Tenant Claim" section needs a follow-up edit replacing the <code>sandbox</code>/<code>customer</code> suggested identifiers with this taxonomy. Not made by this ADR itself — tracked as follow-up so the change is reviewable on its own.</li><li><code>key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md</code> already reflects this decision (<code>tenant:friendly:binky</code>).</li><li>Future tenant onboarding work should classify a tenant against this list before minting an identifier, rather than reaching for a role word.</li><li>Tenant identifiers never change when current grouping changes. The middle segment is creation-time history; <code>tenant-engine</code> is authoritative for the current grouping value.</li><li>Policy and commercial consumers, including spend-ceiling resolution, MUST query <code>tenant-engine</code> and MUST NOT derive current grouping by splitting a tenant identifier.</li><li>The capability-role model (<code>PLTF</code>/<code>IAM</code>/<code>VEN</code>/<code>CUS</code>) remains a separate dimension, now ratified by ADR-0014. Role metadata is carried alongside — not instead of — current grouping and the historical identifier segment.</li><li><code>tenant:platform</code> and <code>tenant:coulomb</code> are reserved outside the taxonomy, pending Bernd's explicit confirmation (see Decision).</li></ul>
|
||||
</section>
|
||||
<section id="alternatives-considered"><h2>Alternatives Considered</h2>
|
||||
<h3>Keep using capability-role words as the tenant grouping</h3>
|
||||
<p>Rejected: tenants are already known to hold multiple roles at once (Binky is <code>CUS</code> now, <code>VEN</code> later); a role-word grouping would need renaming as a tenant's roles evolve, which tenant identifiers are specifically expensive to do once tokens, OpenBao paths, and config reference them.</p>
|
||||
<h3>Retrofit <code>tenant:coulomb</code> into the new taxonomy</h3>
|
||||
<p>Considered <code>tenant:friendly:coulomb</code> or similar. Rejected for now: Coulomb is structurally the reference tenant from ADR-0006, predating this taxonomy, and none of the twelve entity-shape groupings describe "the platform's own ecosystem-development tenant." Revisit if a future grouping is ever added that genuinely fits it.</p>
|
||||
<h3>Do nothing / keep the four original suggested identifiers</h3>
|
||||
<p>Rejected: <code>tenant:customer:binky</code> was the working default going into Binky's onboarding despite the role-collision problem above; the fleet needs this resolved before the first non-Coulomb tenant goes live, not after.</p>
|
||||
</section>
|
||||
<section id="follow-up"><h2>Follow-Up</h2>
|
||||
<ul><li>Edit <code>canon/standards/iam-profile_v0.2.md</code>'s Tenant Claim section to replace the old suggested identifiers with this taxonomy (separate, reviewable change).</li><li>Confirm the <code>tenant:platform</code>/<code>tenant:coulomb</code> reserved/ungrouped treatment explicitly.</li><li>ADR-0014 and the Tenant Engine Boundary Contract define how capability-role metadata is carried alongside grouping.</li><li>Keep <code>tenant-engine</code>'s identifier parser vocabulary-validating for creation and lookup compatibility, but do not expose parsed grouping as current classification.</li></ul>
|
||||
</section><footer><span>NK-ADR-0013 · 2 · accepted</span><span>net-kingdom · docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md · ce198fc2905687ea90a2892346b6860281ac87f8</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="868f953688988b11ce48f4141833bbca51abdb4e86d5b495a8a905002830d7b0">
|
||||
<title>ADR-0007 — Build-stage permissiveness stops at credential disclosure</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0007</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-19</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0007 — Build-stage permissiveness stops at credential disclosure</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-19</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-19, alongside grading the last 14 ungraded catalog lanes.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>ADR-0006</code> deferred a global fail-closed authorization gate because uniform enforcement across an estate under deep refactor hardens the access needed to perform the refactor. The organization's declared posture is <code>build</code> (WP-0029), and the operator has confirmed the estate need not be tight yet.</p>
|
||||
<p>That is correct, and it is also the kind of principle that quietly generalises past its warrant. Read loosely, "we are in build stage" argues for relaxing every control, including the ones that stop a credential landing in a logged agent transcript. Those are not the same class of control, and the difference is not severity — it is <strong>cost</strong>.</p>
|
||||
<p><code>RISK-F-0003</code> made the distinction concrete. <code>ADR-0004</code> reads as a categorical rule: high-risk lanes refuse raw value streaming to agent sessions. The implementation was <code>risk == "high"</code> against an <strong>optional</strong> field, so 14 of 27 lanes never reached the control at all — five of them <code>exec_capable</code>. The control had not been relaxed by anyone's decision. It had simply never been reached, which is worse, because nothing announced it.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>Build-stage permissiveness applies to controls that gate work. It does not apply to controls that prevent credential disclosure.</strong></p>
|
||||
<p>The test is friction, not severity:</p>
|
||||
<ul><li>A control that can <strong>block a legitimate operation</strong> — a fail-closed authorization gate, an enforcement stance — is a candidate for relaxation while the organization is in <code>build</code>, and <code>ADR-0006</code> scopes that relaxation to zones.</li><li>A control that <strong>redirects how a value moves without preventing the work</strong> — the agent read-boundary, which refuses raw stdout but leaves <code>--out</code>, <code>--exec</code>, <code>--wrap</code> and <code>--fingerprint</code> fully available — is not relaxed by build posture, because relaxing it buys nothing. Nobody is unblocked by it.</li></ul>
|
||||
<p>The asymmetry that settles it: a blocked operation is recovered by retrying. A credential written into a logged transcript is not recovered by rotation — rotation limits the damage, it does not unwrite the log. The 2026-07-16 disclosure is the case in point.</p>
|
||||
<p><strong>Therefore, regardless of <code>organization_posture</code>:</strong></p>
|
||||
<ol><li>Every catalog lane carries an explicit <code>risk</code> grade. <strong>Absence is not a grade</strong>, and a lane that omits it is a defect, not a default.</li><li>Grading is done on merit, per lane. This decision is not licence to grade everything <code>high</code> — an over-broad grade is its own inaccuracy, and <code>tenancy-posture</code> §6's <em>accuracy, not altitude</em> applies to this field too.</li><li>Minimum credential-handling standards — the read-boundary, the safe fetch transports, the no-secret audit guard — hold in every posture.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><strong>We accept</strong> the grading cost, now and on every new lane. That is the point: <code>WARDEN-WP-0032-T06</code> makes an ungraded lane impossible rather than merely discouraged, because a rule enforced by remembering is not enforced.</p>
|
||||
<p><strong>We reject</strong> "build stage" as a general argument in credential-handling discussions. It is a real and useful argument about <em>gating</em>, and citing it against a disclosure control is a category error this record exists to name.</p>
|
||||
<p><strong>We note what this decision is not.</strong> It does not set severity for <code>RISK-F-0003</code> — that is <code>risk-nexus</code>'s. It does not make ops-warden the judge of other repos' controls. And it does not survive contact with a zone model that says otherwise: when <code>zone-engine</code> defines admission standards, a zone may legitimately require <em>more</em> than this floor. It may not require less.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>ADR-0004</code> — high-risk lanes refuse raw value streaming to agent sessions</li><li><code>ADR-0006</code> — enforcement is zone-scoped, never a global flag</li><li><code>RISK-F-0003</code> — the read-boundary blind spot that prompted this</li><li><code>WARDEN-WP-0032-T05</code> / <code>T06</code> — the grading, and making absence impossible</li><li><code>zone-engine</code> <code>ZONE-WP-0001</code> — where admission standards will be defined</li></ul>
|
||||
</section><footer><span>ops-warden-adr-0007 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="868f953688988b11ce48f4141833bbca51abdb4e86d5b495a8a905002830d7b0">
|
||||
<title>ADR-0007 — Build-stage permissiveness stops at credential disclosure</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0007</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-19</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0007 — Build-stage permissiveness stops at credential disclosure</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-19</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-19, alongside grading the last 14 ungraded catalog lanes.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>ADR-0006</code> deferred a global fail-closed authorization gate because uniform enforcement across an estate under deep refactor hardens the access needed to perform the refactor. The organization's declared posture is <code>build</code> (WP-0029), and the operator has confirmed the estate need not be tight yet.</p>
|
||||
<p>That is correct, and it is also the kind of principle that quietly generalises past its warrant. Read loosely, "we are in build stage" argues for relaxing every control, including the ones that stop a credential landing in a logged agent transcript. Those are not the same class of control, and the difference is not severity — it is <strong>cost</strong>.</p>
|
||||
<p><code>RISK-F-0003</code> made the distinction concrete. <code>ADR-0004</code> reads as a categorical rule: high-risk lanes refuse raw value streaming to agent sessions. The implementation was <code>risk == "high"</code> against an <strong>optional</strong> field, so 14 of 27 lanes never reached the control at all — five of them <code>exec_capable</code>. The control had not been relaxed by anyone's decision. It had simply never been reached, which is worse, because nothing announced it.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>Build-stage permissiveness applies to controls that gate work. It does not apply to controls that prevent credential disclosure.</strong></p>
|
||||
<p>The test is friction, not severity:</p>
|
||||
<ul><li>A control that can <strong>block a legitimate operation</strong> — a fail-closed authorization gate, an enforcement stance — is a candidate for relaxation while the organization is in <code>build</code>, and <code>ADR-0006</code> scopes that relaxation to zones.</li><li>A control that <strong>redirects how a value moves without preventing the work</strong> — the agent read-boundary, which refuses raw stdout but leaves <code>--out</code>, <code>--exec</code>, <code>--wrap</code> and <code>--fingerprint</code> fully available — is not relaxed by build posture, because relaxing it buys nothing. Nobody is unblocked by it.</li></ul>
|
||||
<p>The asymmetry that settles it: a blocked operation is recovered by retrying. A credential written into a logged transcript is not recovered by rotation — rotation limits the damage, it does not unwrite the log. The 2026-07-16 disclosure is the case in point.</p>
|
||||
<p><strong>Therefore, regardless of <code>organization_posture</code>:</strong></p>
|
||||
<ol><li>Every catalog lane carries an explicit <code>risk</code> grade. <strong>Absence is not a grade</strong>, and a lane that omits it is a defect, not a default.</li><li>Grading is done on merit, per lane. This decision is not licence to grade everything <code>high</code> — an over-broad grade is its own inaccuracy, and <code>tenancy-posture</code> §6's <em>accuracy, not altitude</em> applies to this field too.</li><li>Minimum credential-handling standards — the read-boundary, the safe fetch transports, the no-secret audit guard — hold in every posture.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><strong>We accept</strong> the grading cost, now and on every new lane. That is the point: <code>WARDEN-WP-0032-T06</code> makes an ungraded lane impossible rather than merely discouraged, because a rule enforced by remembering is not enforced.</p>
|
||||
<p><strong>We reject</strong> "build stage" as a general argument in credential-handling discussions. It is a real and useful argument about <em>gating</em>, and citing it against a disclosure control is a category error this record exists to name.</p>
|
||||
<p><strong>We note what this decision is not.</strong> It does not set severity for <code>RISK-F-0003</code> — that is <code>risk-nexus</code>'s. It does not make ops-warden the judge of other repos' controls. And it does not survive contact with a zone model that says otherwise: when <code>zone-engine</code> defines admission standards, a zone may legitimately require <em>more</em> than this floor. It may not require less.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>ADR-0004</code> — high-risk lanes refuse raw value streaming to agent sessions</li><li><code>ADR-0006</code> — enforcement is zone-scoped, never a global flag</li><li><code>RISK-F-0003</code> — the read-boundary blind spot that prompted this</li><li><code>WARDEN-WP-0032-T05</code> / <code>T06</code> — the grading, and making absence impossible</li><li><code>zone-engine</code> <code>ZONE-WP-0001</code> — where admission standards will be defined</li></ul>
|
||||
</section><footer><span>ops-warden-adr-0007 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="7df0bb364276e382cbee9383e7e67d399b0ac1b0353246e0ab323e629e732a6d">
|
||||
<title>ADR-0001 — The routing catalog is a pointer layer, never a second copy</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0001</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0001 — The routing catalog is a pointer layer, never a second copy</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0001-catalog-is-a-pointer-layer.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0001</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0001 — The routing catalog is a pointer layer, never a second copy</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0001-catalog-is-a-pointer-layer.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted. Decided during WARDEN-WP-0010 (access routing charter), enforced in code since WARDEN-WP-0011. Restated here because it binds repos other than ops-warden and had, until now, no address they could cite.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -213,4 +213,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>registry/routing/catalog.yaml</code> — the file this governs, header comment</li><li><code>wiki/AccessRouting.md</code> — the issue-vs-route role and boundary</li><li><code>ADR-0005</code> — the narrower charter this follows from</li></ul>
|
||||
</section><footer><span>ops-warden-adr-0001 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0001-catalog-is-a-pointer-layer.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</span></footer></main></div></div></html>
|
||||
</section><footer><span>ops-warden-adr-0001 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0001-catalog-is-a-pointer-layer.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="7dcc31732d774ddf2c98636b69ee12e2d74034836ed0def81b7e06461309b53a">
|
||||
<title>ADR-0002 — ops-warden is a transparent conduit, never a secret broker</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0002</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0002 — ops-warden is a transparent conduit, never a secret broker</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0002-conduit-not-broker.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0002</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0002 — ops-warden is a transparent conduit, never a secret broker</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0002-conduit-not-broker.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted. Decided during WARDEN-WP-0014 (operator access assist), tightened by WARDEN-WP-0026 (disclosure hygiene).</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -213,4 +213,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>wiki/OperatorAccessAssist.md#the-conduit-vs-broker-boundary-the-security-model</code></li><li><code>ADR-0004</code> — the agent-session read boundary built on top of this</li><li><code>ADR-0003</code> — why proxied lanes are tracked as interim rather than owned</li></ul>
|
||||
</section><footer><span>ops-warden-adr-0002 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0002-conduit-not-broker.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</span></footer></main></div></div></html>
|
||||
</section><footer><span>ops-warden-adr-0002 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0002-conduit-not-broker.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="45b47c02afa575fbfe5be980e426c331a1d99bb9cd9c7a8de80bf8e319469f81">
|
||||
<title>ADR-0003 — Cover gaps, but never silently own them</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0003</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0003 — Cover gaps, but never silently own them</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0003</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0003 — Cover gaps, but never silently own them</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted. Stated as INTENT §9, made structural by WARDEN-WP-0030 (delegation register).</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -215,4 +215,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>INTENT.md</code> §9 — the principle this formalizes</li><li><code>history/2026-08-11-delegation-surface-assessment.md</code> — the assessment that forced it</li><li><code>.claude/rules/finding-routing.md</code> — the register-versus-findings boundary</li></ul>
|
||||
</section><footer><span>ops-warden-adr-0003 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</span></footer></main></div></div></html>
|
||||
</section><footer><span>ops-warden-adr-0003 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
|
|||
215
build/adr/ops-warden-grade-disclosure-path/v1/index.html
Normal file
215
build/adr/ops-warden-grade-disclosure-path/v1/index.html
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="2e22863ba592802cceb40b32d395168b42ace747741f5188307505eaa89ff764">
|
||||
<title>ADR-0008 — A lane's risk grade covers every field its path discloses</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0008</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-21</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0008 — A lane's risk grade covers every field its path discloses</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0008-grade-the-path-not-the-field.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-21</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-21, after <code>secrets-engine</code> found two under-graded lanes while reviewing ops-warden's own catalog metadata.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>ADR-0007</code> requires every catalog lane to carry an explicit <code>risk</code> grade. It does not say what the grade is <em>of</em>, and the omission turned out to matter.</p>
|
||||
<p>The catalog describes a lane by a single <code>fetch_command</code> naming a single field — <code>bao kv get -field=ISSUE_CORE_API_KEY <path></code>. Grading followed that description. But the unit of disclosure is not the field, it is the <strong>path</strong>: <code>bao kv get</code> without <code>-field</code> returns every key stored there, and an agent session that discloses one field has disclosed all of them.</p>
|
||||
<p>On 2026-08-19, grading all 27 lanes, ops-warden graded <code>issue-core-ingestion-api-key</code> and <code>reuse-surface-hub-write-token</code> as <code>standard</code> — "ordinary internal workload secrets". Both grades read only the headline field. <code>CCR-2026-0002</code> records a deliberate decision to keep <code>GITEA_BACKEND_TOKEN</code> at the first path; <code>CCR-2026-0005</code> declares a dual-consumer webhook HMAC at the second. Neither is recovered by rotating the credential the lane is named after.</p>
|
||||
<p>Three details make this worth a record rather than a fix:</p>
|
||||
<ul><li><strong>The evidence was already ours.</strong> The field sets were in the CCRs the catalog already cites as authoritative. This was not missing data; it was unread data.</li><li><strong>A test held the error still.</strong> <code>test_high_risk_lanes_classified</code> asserted <code>issue-core-ingestion-api-key</code> was <em>not</em> high. A first grading pass had marked it high, the test contradicted it, and the test was believed. A test that encodes a judgement defends that judgement from correction.</li><li><strong>Another repo found it.</strong> <code>secrets-engine</code> graded both <code>high</code> independently while drafting catalog entries whose schema records <code>fields</code>. A schema that names the field set makes the right grade obvious; ours did not have one.</li></ul>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>A lane's <code>risk</code> grade is a property of its path, and must cover the union of everything a read of that path would disclose.</strong></p>
|
||||
<ol><li>Where the field set is known, the catalog records it as <code>fields</code>, with the authority it came from.</li><li>The grade is argued against the most damaging field, not the named one.</li><li>Where the field set is unknown, that is stated — never assumed to be one field. An unverified field set is a reason to grade conservatively, matching the <code>inter-hub-bootstrap-ssh</code> precedent under <code>ADR-0007</code>.</li><li>Establishing a field set must not be done by reading the secret. Use the owning CCR, the owner's catalog, or <code>bao kv metadata</code>. <code>bao kv get</code> on a high-risk path is the 2026-07-16 vector and is forbidden by <code>ADR-0004</code> for agent sessions regardless of intent.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><code>ADR-0007</code> is unchanged and still governs: every lane carries an explicit grade, and absence fails safe. This record says what that grade must account for.</p>
|
||||
<p>Grading gets more expensive: it now requires knowing what is at a path, not just what the lane is called. That cost is the point — the cheap version produced two wrong answers in one pass and is the reason this exists.</p>
|
||||
<p>A test that asserts a grade is asserting a judgement. When a grade is disputed, re-argue it from evidence before trusting the test that encodes it.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>ADR-0007</code> — every lane carries an explicit grade; build-stage permissiveness stops at credential disclosure</li><li><code>ADR-0004</code> — high-risk lanes refuse raw value streaming to agent sessions</li><li><code>ADR-0001</code> — the catalog is a pointer layer; <code>fields</code> records the owner's declared field set with its source, and does not restate their procedure</li><li><code>WARDEN-WP-0033-T02</code>; <code>secrets-engine</code> <code>SECRETS-WP-0006</code></li><li><code>history/2026-07-16-credential-disclosure-lessons.md</code></li></ul>
|
||||
</section><footer><span>ops-warden-adr-0008 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0008-grade-the-path-not-the-field.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="2e22863ba592802cceb40b32d395168b42ace747741f5188307505eaa89ff764">
|
||||
<title>ADR-0008 — A lane's risk grade covers every field its path discloses</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0008</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-21</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0008 — A lane's risk grade covers every field its path discloses</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0008-grade-the-path-not-the-field.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-21</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-21, after <code>secrets-engine</code> found two under-graded lanes while reviewing ops-warden's own catalog metadata.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>ADR-0007</code> requires every catalog lane to carry an explicit <code>risk</code> grade. It does not say what the grade is <em>of</em>, and the omission turned out to matter.</p>
|
||||
<p>The catalog describes a lane by a single <code>fetch_command</code> naming a single field — <code>bao kv get -field=ISSUE_CORE_API_KEY <path></code>. Grading followed that description. But the unit of disclosure is not the field, it is the <strong>path</strong>: <code>bao kv get</code> without <code>-field</code> returns every key stored there, and an agent session that discloses one field has disclosed all of them.</p>
|
||||
<p>On 2026-08-19, grading all 27 lanes, ops-warden graded <code>issue-core-ingestion-api-key</code> and <code>reuse-surface-hub-write-token</code> as <code>standard</code> — "ordinary internal workload secrets". Both grades read only the headline field. <code>CCR-2026-0002</code> records a deliberate decision to keep <code>GITEA_BACKEND_TOKEN</code> at the first path; <code>CCR-2026-0005</code> declares a dual-consumer webhook HMAC at the second. Neither is recovered by rotating the credential the lane is named after.</p>
|
||||
<p>Three details make this worth a record rather than a fix:</p>
|
||||
<ul><li><strong>The evidence was already ours.</strong> The field sets were in the CCRs the catalog already cites as authoritative. This was not missing data; it was unread data.</li><li><strong>A test held the error still.</strong> <code>test_high_risk_lanes_classified</code> asserted <code>issue-core-ingestion-api-key</code> was <em>not</em> high. A first grading pass had marked it high, the test contradicted it, and the test was believed. A test that encodes a judgement defends that judgement from correction.</li><li><strong>Another repo found it.</strong> <code>secrets-engine</code> graded both <code>high</code> independently while drafting catalog entries whose schema records <code>fields</code>. A schema that names the field set makes the right grade obvious; ours did not have one.</li></ul>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>A lane's <code>risk</code> grade is a property of its path, and must cover the union of everything a read of that path would disclose.</strong></p>
|
||||
<ol><li>Where the field set is known, the catalog records it as <code>fields</code>, with the authority it came from.</li><li>The grade is argued against the most damaging field, not the named one.</li><li>Where the field set is unknown, that is stated — never assumed to be one field. An unverified field set is a reason to grade conservatively, matching the <code>inter-hub-bootstrap-ssh</code> precedent under <code>ADR-0007</code>.</li><li>Establishing a field set must not be done by reading the secret. Use the owning CCR, the owner's catalog, or <code>bao kv metadata</code>. <code>bao kv get</code> on a high-risk path is the 2026-07-16 vector and is forbidden by <code>ADR-0004</code> for agent sessions regardless of intent.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p><code>ADR-0007</code> is unchanged and still governs: every lane carries an explicit grade, and absence fails safe. This record says what that grade must account for.</p>
|
||||
<p>Grading gets more expensive: it now requires knowing what is at a path, not just what the lane is called. That cost is the point — the cheap version produced two wrong answers in one pass and is the reason this exists.</p>
|
||||
<p>A test that asserts a grade is asserting a judgement. When a grade is disputed, re-argue it from evidence before trusting the test that encodes it.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>ADR-0007</code> — every lane carries an explicit grade; build-stage permissiveness stops at credential disclosure</li><li><code>ADR-0004</code> — high-risk lanes refuse raw value streaming to agent sessions</li><li><code>ADR-0001</code> — the catalog is a pointer layer; <code>fields</code> records the owner's declared field set with its source, and does not restate their procedure</li><li><code>WARDEN-WP-0033-T02</code>; <code>secrets-engine</code> <code>SECRETS-WP-0006</code></li><li><code>history/2026-07-16-credential-disclosure-lessons.md</code></li></ul>
|
||||
</section><footer><span>ops-warden-adr-0008 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0008-grade-the-path-not-the-field.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="5db38dcb754af1ba639f4ceca056df842bc7b91fa48dd8bad21611aee29f682d">
|
||||
<title>ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0004</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0004</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted. Decided during WARDEN-WP-0026 (credential disclosure hygiene), in response to a real disclosure on 2026-07-16.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -213,4 +213,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>wiki/playbooks/agent-read-boundary.md</code></li><li><code>wiki/playbooks/exposed-taint.md</code></li><li><code>ADR-0002</code> — the conduit rule this narrows for agent callers</li></ul>
|
||||
</section><footer><span>ops-warden-adr-0004 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</span></footer></main></div></div></html>
|
||||
</section><footer><span>ops-warden-adr-0004 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="31eafe4d8d9362a3446739d63c3af83fd9138cfdc6ad120086312a67dc0d27d0">
|
||||
<title>ADR-0005 — Implement one lane narrowly, route everything else</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0005</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0005 — Implement one lane narrowly, route everything else</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0005-implement-narrowly-route-broadly.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0005</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0005 — Implement one lane narrowly, route everything else</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0005-implement-narrowly-route-broadly.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2027-02-18</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted. The founding charter decision, taken 2026-06-18 (<code>history/2026-06-18-access-routing-intent-shift-assessment.md</code>).</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
|
|
@ -211,4 +211,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>SCOPE.md</code> — the issue-vs-route table</li><li><code>wiki/AccessRouting.md</code> — role and boundary</li><li><code>ADR-0001</code>, <code>ADR-0002</code>, <code>ADR-0003</code> — the three rules that follow from this one</li></ul>
|
||||
</section><footer><span>ops-warden-adr-0005 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0005-implement-narrowly-route-broadly.md · 35aff380a33f51a512c1e1b42d52d1dc0d95930f</span></footer></main></div></div></html>
|
||||
</section><footer><span>ops-warden-adr-0005 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0005-implement-narrowly-route-broadly.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
|
|||
215
build/adr/ops-warden-security-zones-consumer/v1/index.html
Normal file
215
build/adr/ops-warden-security-zones-consumer/v1/index.html
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="73fff22177b4bec2c56ff737e06e4225eb02d39669be3ea1b723906245f878b8">
|
||||
<title>ADR-0009 — Adopt security-zones v0.1 as a consumer</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0009</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0009 — Adopt security-zones v0.1 as a consumer</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2026-11-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-22 after zone-engine completed <code>ZONE-WP-0001-T03/T05</code> and published the declaration, compilation, stance, and failure-mode contract in canon revision <code>337484a</code>; zone-engine's reference compiler is revision <code>9b6ada7</code>.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0006 rejected a repo-wide <code>policy.enabled</code> switch because one boolean plus one <code>fail_closed</code> value made flex-auth a uniform dependency of every signing path, including continuity paths needed to repair that dependency. It deferred the replacement to zone-engine rather than designing an estate model here.</p>
|
||||
<p>The owning model now exists. A zone is an evidenced workload-admission fact; control stance remains with the control owner, and dependency failure behavior remains with the PEP. Membership resolves only through an authoritative workload identity. Missing identity, membership, admission evidence, or a required floor is <code>unknown</code>, never an inferred permissive zone.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Ops-warden adopts <code>security-zones_v0.1</code> and accepts its initial build-stage rows for the controls ops-warden owns:</p>
|
||||
<ul><li>the pre-sign PEP fails open for <code>z0-experimental</code>, <code>z1-operational</code>, <code>z2-protected</code>, <code>z2-continuity</code>, and build-profile <code>unknown</code>; it fails closed for <code>z3-critical</code>;</li><li>the agent high-risk read boundary remains enforced and fail-closed in every zone and for <code>unknown</code>;</li><li><code>warden plan</code> never derives <code>autonomous</code> authority from unknown zone evidence.</li></ul>
|
||||
<p>The implementation follows four rules:</p>
|
||||
<ol><li><code>policy.enabled</code> and the global <code>policy.fail_closed</code> setting are retired and rejected by configuration loading. The PEP chooses failure behavior from a total per-zone map.</li><li>The existing compiled flex-auth registry is the resource-membership carrier. Actor resources receive <code>workload_id</code>, <code>security_zone</code>, <code>security_zone_admission</code>, and <code>security_zone_revision</code>. The dormant <code>trust_zone: platform</code> constant is removed; it is not repurposed.</li><li>Workload joins are explicit. Managed deployables use Repo Manager's exact <code>(rapp_id, workload_identity.name, deployable?)</code> tuple. Independent operational workloads use their owner-reviewed <code>tenancy.yaml</code>. Catalog owners distinguish <code>not-applicable</code> from applicable-but-<code>unknown</code>; no path or repository-name inference is allowed.</li><li>A fail-open signing result is metadata, not silence. Signature and unified audit records carry the selected zone, failure mode, outcome, and decision id when one exists.</li></ol>
|
||||
<p>Ops-warden itself declares <code>z1-operational</code>. That is an accuracy decision: the workload has M1 evidence and does not yet have the SLO history, on-call rotation, or exercised recovery evidence needed for z2 admission.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p>The global flip and its failure cycle no longer exist. An unknown target remains observable and follows the versioned build profile without manufacturing membership. A future organization-posture graduation changes the versioned control profile, not each workload declaration.</p>
|
||||
<p>The flex-auth policy package still owns pre-sign stance. Ops-warden can compile and send the membership attributes, handle <code>allow</code>/<code>audit_only</code>/deny, and apply the correct PEP failure mode; it does not write flex-auth's Rego rows.</p>
|
||||
<p>Catalog coverage is intentionally honest at adoption: exact references resolve where authoritative declarations exist, applicable lanes without one report <code>unknown</code> with a reason, and generic actions/patterns are explicitly <code>not-applicable</code>. Resolution coverage improves by adding owner declarations, never by adding heuristics here.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>security-zones_v0.1</code> (net-kingdom canon revision <code>337484a</code>; zone-engine compiler revision <code>9b6ada7</code>)</li><li>Repo Manager <code>helixforge.workloads.ops-warden-reference.v1</code> revision <code>890f3b0</code></li><li>NetKingdom tenancy-posture Decisions 5.6.1/5.6.2</li><li><code>WARDEN-WP-0032</code></li><li><code>ADR-0004</code>, <code>ADR-0007</code>, and <code>ADR-0008</code></li></ul>
|
||||
</section><footer><span>ops-warden-adr-0009 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="73fff22177b4bec2c56ff737e06e4225eb02d39669be3ea1b723906245f878b8">
|
||||
<title>ADR-0009 — Adopt security-zones v0.1 as a consumer</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0009</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0009 — Adopt security-zones v0.1 as a consumer</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2026-11-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-22 after zone-engine completed <code>ZONE-WP-0001-T03/T05</code> and published the declaration, compilation, stance, and failure-mode contract in canon revision <code>337484a</code>; zone-engine's reference compiler is revision <code>9b6ada7</code>.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0006 rejected a repo-wide <code>policy.enabled</code> switch because one boolean plus one <code>fail_closed</code> value made flex-auth a uniform dependency of every signing path, including continuity paths needed to repair that dependency. It deferred the replacement to zone-engine rather than designing an estate model here.</p>
|
||||
<p>The owning model now exists. A zone is an evidenced workload-admission fact; control stance remains with the control owner, and dependency failure behavior remains with the PEP. Membership resolves only through an authoritative workload identity. Missing identity, membership, admission evidence, or a required floor is <code>unknown</code>, never an inferred permissive zone.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Ops-warden adopts <code>security-zones_v0.1</code> and accepts its initial build-stage rows for the controls ops-warden owns:</p>
|
||||
<ul><li>the pre-sign PEP fails open for <code>z0-experimental</code>, <code>z1-operational</code>, <code>z2-protected</code>, <code>z2-continuity</code>, and build-profile <code>unknown</code>; it fails closed for <code>z3-critical</code>;</li><li>the agent high-risk read boundary remains enforced and fail-closed in every zone and for <code>unknown</code>;</li><li><code>warden plan</code> never derives <code>autonomous</code> authority from unknown zone evidence.</li></ul>
|
||||
<p>The implementation follows four rules:</p>
|
||||
<ol><li><code>policy.enabled</code> and the global <code>policy.fail_closed</code> setting are retired and rejected by configuration loading. The PEP chooses failure behavior from a total per-zone map.</li><li>The existing compiled flex-auth registry is the resource-membership carrier. Actor resources receive <code>workload_id</code>, <code>security_zone</code>, <code>security_zone_admission</code>, and <code>security_zone_revision</code>. The dormant <code>trust_zone: platform</code> constant is removed; it is not repurposed.</li><li>Workload joins are explicit. Managed deployables use Repo Manager's exact <code>(rapp_id, workload_identity.name, deployable?)</code> tuple. Independent operational workloads use their owner-reviewed <code>tenancy.yaml</code>. Catalog owners distinguish <code>not-applicable</code> from applicable-but-<code>unknown</code>; no path or repository-name inference is allowed.</li><li>A fail-open signing result is metadata, not silence. Signature and unified audit records carry the selected zone, failure mode, outcome, and decision id when one exists.</li></ol>
|
||||
<p>Ops-warden itself declares <code>z1-operational</code>. That is an accuracy decision: the workload has M1 evidence and does not yet have the SLO history, on-call rotation, or exercised recovery evidence needed for z2 admission.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p>The global flip and its failure cycle no longer exist. An unknown target remains observable and follows the versioned build profile without manufacturing membership. A future organization-posture graduation changes the versioned control profile, not each workload declaration.</p>
|
||||
<p>The flex-auth policy package still owns pre-sign stance. Ops-warden can compile and send the membership attributes, handle <code>allow</code>/<code>audit_only</code>/deny, and apply the correct PEP failure mode; it does not write flex-auth's Rego rows.</p>
|
||||
<p>Catalog coverage is intentionally honest at adoption: exact references resolve where authoritative declarations exist, applicable lanes without one report <code>unknown</code> with a reason, and generic actions/patterns are explicitly <code>not-applicable</code>. Resolution coverage improves by adding owner declarations, never by adding heuristics here.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>security-zones_v0.1</code> (net-kingdom canon revision <code>337484a</code>; zone-engine compiler revision <code>9b6ada7</code>)</li><li>Repo Manager <code>helixforge.workloads.ops-warden-reference.v1</code> revision <code>890f3b0</code></li><li>NetKingdom tenancy-posture Decisions 5.6.1/5.6.2</li><li><code>WARDEN-WP-0032</code></li><li><code>ADR-0004</code>, <code>ADR-0007</code>, and <code>ADR-0008</code></li></ul>
|
||||
</section><footer><span>ops-warden-adr-0009 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
218
build/adr/ops-warden-staff-layer/v1/index.html
Normal file
218
build/adr/ops-warden-staff-layer/v1/index.html
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="064455bcb2870abf8e243f5a8c154e50bfc1c537cfba7996a792f9e314dd78ae">
|
||||
<title>ADR-0010 — ops-warden is Staff</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0010</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-28</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0010 — ops-warden is Staff</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0010-ops-warden-is-staff.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2026-11-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-28, answering intake <code>WARDEN-IN-0001</code> from gate-house, which carries decision <code>GH-DEC-2026-001</code>. The standard being adopted — <code>net-kingdom/canon/standards/security-layer-model_v0.1.md</code> — is <code>proposed</code>, and was proposed pending assent from flex-auth, kings-guard, and ops-warden. This ADR is ops-warden's half of that assent.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The estate acquired overlapping claims to the same responsibility, most visibly two repositories describing themselves as the authorization control plane. The layer model resolves the overlap by layering repositories on determinism — Taxonomy, Tooling, Engines, Staff — and by two rules: Staff never touches Tooling directly (§5), and <code>access-engine</code> is the only policy decision point (§6).</p>
|
||||
<p>ops-warden is assigned Staff. Two demarcations follow that touch this repository: the security curriculum it had been carrying belongs to gate-house, and the words <em>access lane</em> and <em>access rule</em> are bound to different owners.</p>
|
||||
<p>Full reasoning: <code>history/2026-08-28-security-layer-model-assent.md</code>.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>1. ops-warden is Staff and declares it.</strong> <code>INTENT.md</code> carries the layer label and the §5 invariant. ops-warden holds no state another layer depends on at runtime and renders no authorization decision — it consumes them.</p>
|
||||
<p><strong>2. Lanes, not rules.</strong> ops-warden owns <em>how</em> a worker reaches a host: SSH certificate issuance, the routing catalog, <code>warden access</code>, <code>warden plan</code>, <code>cert_command</code>. It never owns <em>whether</em> a worker may — that is <code>access-engine</code> (today <code>flex-auth</code>), and ops-warden neither renders nor caches that decision. This restates what <code>ADR-0002</code> and <code>ADR-0005</code> already bind; it is recorded here because the demarcation is now normative estate-wide and other repositories rely on ops-warden holding to it. The ruled rename <code>flex-auth</code> → <code>access-engine</code> is assented to; ops-warden asks only for a window in which both names resolve.</p>
|
||||
<p><strong>3. Doctrine goes to gate-house; runbooks stay here.</strong> ops-warden does not restate security doctrine, the authority model, or the curriculum. It references gate-house's. It keeps everything operational about the lanes it stewards: which subsystem owns which need, how to obtain a credential lane by lane, and conformance evidence for its own lanes. <code>.claude/rules/credential-routing.md</code> is runbook, not curriculum, and stays inlined in this and every other repository.</p>
|
||||
<p><strong>4. One declared engine gap, not an exemption.</strong> <code>src/warden/vault.py</code> (<code>VaultCA</code>) is a direct OpenBao client performing a write from a Staff repository. It is a §5 non-conformance. ops-warden declares it rather than arguing it away:</p>
|
||||
<ul><li><strong>intended owner:</strong> <code>secrets-engine</code> (credential abstraction, custody, lifecycle)</li><li><strong>blocked on:</strong> no engine exposes an SSH certificate signing surface</li><li><strong>review:</strong> with this ADR, every 3 months</li></ul>
|
||||
<p>Until that surface exists, ops-warden continues to sign — refusing to would remove production host access to close a documentation gap — and reports the position as open. <code>warden desk</code>'s <code>bao kv put</code> is declared on the same terms. <code>taint.py</code> is metadata-only observation, declared under §5's read-only allowance. <code>proxy.py</code> supplies no authority of its own: it runs the owner's tool under the caller's identity and is governed by <code>ADR-0002</code>.</p>
|
||||
<p>This is <code>ADR-0003</code> turned inward. ops-warden has required an intended owner and a blocker on 27 catalog lanes it holds for other repositories; it holds itself to the same record.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p>ops-warden's conformance under §10 is <em>declared non-conformant with a tracked closure path</em>, not clean. That is the accurate state and it is the state that gets fixed, because it names an owner who can fix it.</p>
|
||||
<p>An amendment to §5 has been offered to gate-house — a second sanctioned shape alongside read-only diagnostics: a declared engine gap carrying intended owner, blocker, and review date, machine-readable so §10 can tell a tracked gap from an undeclared violation. It is offered, not assumed; §5 stays gate-house's to write. If gate-house declines it, ops-warden's position is a plain non-conformance and is reported as one.</p>
|
||||
<p>The <code>NetKingdom Security Literacy</code> section stops being a prose second source for <code>registry/routing/catalog.yaml</code>, which <code>ADR-0001</code> had already ruled against for catalog procedure.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>net-kingdom/canon/standards/security-layer-model_v0.1.md</code> (proposed, gate-house)</li><li><code>gate-house/decisions/decisions.md</code> — <code>GH-DEC-2026-001</code></li><li><code>history/2026-08-28-security-layer-model-assent.md</code></li><li><code>ADR-0001</code>, <code>ADR-0002</code>, <code>ADR-0003</code>, <code>ADR-0005</code>, <code>ADR-0009</code></li><li><code>WARDEN-IN-0001</code></li></ul>
|
||||
</section><footer><span>ops-warden-adr-0010 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0010-ops-warden-is-staff.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
218
build/adr/ops-warden-staff-layer/v1/revisions/1/index.html
Normal file
218
build/adr/ops-warden-staff-layer/v1/revisions/1/index.html
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="4e267179db741b27a3e62f81f753cd9752c97412">
|
||||
<meta name="policy-source-digest" content="064455bcb2870abf8e243f5a8c154e50bfc1c537cfba7996a792f9e314dd78ae">
|
||||
<title>ADR-0010 — ops-warden is Staff</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>ops-warden-adr-0010</span> <span class="stat">accepted · 1</span> <span>ops-warden</span> <span>reviewed 2026-08-28</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0010 — ops-warden is Staff</h1><p class="sub">Source: <code>ops-warden · docs/adr/ADR-0010-ops-warden-is-staff.md · 4e267179db741b27a3e62f81f753cd9752c97412</code></p><p class="sub">Review due: 2026-11-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#related"><span class="n">·</span>Related</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
||||
<p>Accepted 2026-08-28, answering intake <code>WARDEN-IN-0001</code> from gate-house, which carries decision <code>GH-DEC-2026-001</code>. The standard being adopted — <code>net-kingdom/canon/standards/security-layer-model_v0.1.md</code> — is <code>proposed</code>, and was proposed pending assent from flex-auth, kings-guard, and ops-warden. This ADR is ops-warden's half of that assent.</p>
|
||||
</section>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>The estate acquired overlapping claims to the same responsibility, most visibly two repositories describing themselves as the authorization control plane. The layer model resolves the overlap by layering repositories on determinism — Taxonomy, Tooling, Engines, Staff — and by two rules: Staff never touches Tooling directly (§5), and <code>access-engine</code> is the only policy decision point (§6).</p>
|
||||
<p>ops-warden is assigned Staff. Two demarcations follow that touch this repository: the security curriculum it had been carrying belongs to gate-house, and the words <em>access lane</em> and <em>access rule</em> are bound to different owners.</p>
|
||||
<p>Full reasoning: <code>history/2026-08-28-security-layer-model-assent.md</code>.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p><strong>1. ops-warden is Staff and declares it.</strong> <code>INTENT.md</code> carries the layer label and the §5 invariant. ops-warden holds no state another layer depends on at runtime and renders no authorization decision — it consumes them.</p>
|
||||
<p><strong>2. Lanes, not rules.</strong> ops-warden owns <em>how</em> a worker reaches a host: SSH certificate issuance, the routing catalog, <code>warden access</code>, <code>warden plan</code>, <code>cert_command</code>. It never owns <em>whether</em> a worker may — that is <code>access-engine</code> (today <code>flex-auth</code>), and ops-warden neither renders nor caches that decision. This restates what <code>ADR-0002</code> and <code>ADR-0005</code> already bind; it is recorded here because the demarcation is now normative estate-wide and other repositories rely on ops-warden holding to it. The ruled rename <code>flex-auth</code> → <code>access-engine</code> is assented to; ops-warden asks only for a window in which both names resolve.</p>
|
||||
<p><strong>3. Doctrine goes to gate-house; runbooks stay here.</strong> ops-warden does not restate security doctrine, the authority model, or the curriculum. It references gate-house's. It keeps everything operational about the lanes it stewards: which subsystem owns which need, how to obtain a credential lane by lane, and conformance evidence for its own lanes. <code>.claude/rules/credential-routing.md</code> is runbook, not curriculum, and stays inlined in this and every other repository.</p>
|
||||
<p><strong>4. One declared engine gap, not an exemption.</strong> <code>src/warden/vault.py</code> (<code>VaultCA</code>) is a direct OpenBao client performing a write from a Staff repository. It is a §5 non-conformance. ops-warden declares it rather than arguing it away:</p>
|
||||
<ul><li><strong>intended owner:</strong> <code>secrets-engine</code> (credential abstraction, custody, lifecycle)</li><li><strong>blocked on:</strong> no engine exposes an SSH certificate signing surface</li><li><strong>review:</strong> with this ADR, every 3 months</li></ul>
|
||||
<p>Until that surface exists, ops-warden continues to sign — refusing to would remove production host access to close a documentation gap — and reports the position as open. <code>warden desk</code>'s <code>bao kv put</code> is declared on the same terms. <code>taint.py</code> is metadata-only observation, declared under §5's read-only allowance. <code>proxy.py</code> supplies no authority of its own: it runs the owner's tool under the caller's identity and is governed by <code>ADR-0002</code>.</p>
|
||||
<p>This is <code>ADR-0003</code> turned inward. ops-warden has required an intended owner and a blocker on 27 catalog lanes it holds for other repositories; it holds itself to the same record.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<p>ops-warden's conformance under §10 is <em>declared non-conformant with a tracked closure path</em>, not clean. That is the accurate state and it is the state that gets fixed, because it names an owner who can fix it.</p>
|
||||
<p>An amendment to §5 has been offered to gate-house — a second sanctioned shape alongside read-only diagnostics: a declared engine gap carrying intended owner, blocker, and review date, machine-readable so §10 can tell a tracked gap from an undeclared violation. It is offered, not assumed; §5 stays gate-house's to write. If gate-house declines it, ops-warden's position is a plain non-conformance and is reported as one.</p>
|
||||
<p>The <code>NetKingdom Security Literacy</code> section stops being a prose second source for <code>registry/routing/catalog.yaml</code>, which <code>ADR-0001</code> had already ruled against for catalog procedure.</p>
|
||||
</section>
|
||||
<section id="related"><h2>Related</h2>
|
||||
<ul><li><code>net-kingdom/canon/standards/security-layer-model_v0.1.md</code> (proposed, gate-house)</li><li><code>gate-house/decisions/decisions.md</code> — <code>GH-DEC-2026-001</code></li><li><code>history/2026-08-28-security-layer-model-assent.md</code></li><li><code>ADR-0001</code>, <code>ADR-0002</code>, <code>ADR-0003</code>, <code>ADR-0005</code>, <code>ADR-0009</code></li><li><code>WARDEN-IN-0001</code></li></ul>
|
||||
</section><footer><span>ops-warden-adr-0010 · 1 · accepted</span><span>ops-warden · docs/adr/ADR-0010-ops-warden-is-staff.md · 4e267179db741b27a3e62f81f753cd9752c97412</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="56d516e10cdad6691f254ec9cb11f11ea364f7de">
|
||||
<meta name="policy-source-revision" content="e5f3497337575e1fd85fe2bfde5b2183c690d94e">
|
||||
<meta name="policy-source-digest" content="9b12ab6aa0e6f9eba03465782c35d6ff4683b682191599e38f4f9614cde9fa1b">
|
||||
<title>ADR-0003 — Decisions that bind others live in docs/adr, not only in the State Hub</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RPLAT-ADR-0003</span> <span class="stat">accepted · 1.0</span> <span>railiance-platform</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0003 — Decisions that bind others live in docs/adr, not only in the State Hub</h1><p class="sub">Source: <code>railiance-platform · docs/adr/ADR-0003-decisions-live-in-the-repo.md · 56d516e10cdad6691f254ec9cb11f11ea364f7de</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li></ol></nav><main><section id="context"><h2>Context</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RPLAT-ADR-0003</span> <span class="stat">accepted · 1.0</span> <span>railiance-platform</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0003 — Decisions that bind others live in docs/adr, not only in the State Hub</h1><p class="sub">Source: <code>railiance-platform · docs/adr/ADR-0003-decisions-live-in-the-repo.md · e5f3497337575e1fd85fe2bfde5b2183c690d94e</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li></ol></nav><main><section id="context"><h2>Context</h2>
|
||||
<p>This repo recorded decisions with the State Hub's <code>record_decision()</code> and wrote governing content as prose in <code>docs/</code> — 24 files on 2026-08-17, none carrying a status, owner, revision or review date. It held no ADRs at all.</p>
|
||||
<p>Two things made that a defect rather than a style.</p>
|
||||
<p><strong>The hub is a read model.</strong> The estate's standing rule is that local files are the source of truth and the hub reflects them. A decision that exists only as a hub record inverts that for the one class of content where it matters most.</p>
|
||||
|
|
@ -208,4 +208,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<section id="alternatives-considered"><h2>Alternatives considered</h2>
|
||||
<p><strong>Keep decisions in the hub and have <code>policy-nexus</code> read it.</strong> Rejected on both sides: it would make a read model authoritative, and it would give the publication surface a source that no repo can diff or review.</p>
|
||||
<p><strong>Add frontmatter to all 24 existing <code>docs/</code> files.</strong> Rejected. Most are runbooks that should not be published, and stamping them with a status would assert a decision that was never made.</p>
|
||||
</section><footer><span>RPLAT-ADR-0003 · 1.0 · accepted</span><span>railiance-platform · docs/adr/ADR-0003-decisions-live-in-the-repo.md · 56d516e10cdad6691f254ec9cb11f11ea364f7de</span></footer></main></div></div></html>
|
||||
</section><footer><span>RPLAT-ADR-0003 · 1.0 · accepted</span><span>railiance-platform · docs/adr/ADR-0003-decisions-live-in-the-repo.md · e5f3497337575e1fd85fe2bfde5b2183c690d94e</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="e02982ce54691cf1589ac3d04012b9b9282f9eb54ed4b8f2f0544371f9b87f3e">
|
||||
<title>Derived Rail Composition</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0005</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-26</span><span>generated from canonical source — do not edit</span></div><h1>Derived Rail Composition</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0005-derived-rail-composition.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-01-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-07-26 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0005</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-26</span><span>generated from canonical source — do not edit</span></div><h1>Derived Rail Composition</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0005-derived-rail-composition.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-01-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-07-26 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Knative provides distinct workload activation and revision semantics but runs on Kubernetes. Treating it as an unrelated peer rail would duplicate generic workload lifecycle and substrate assumptions.</p>
|
||||
</section>
|
||||
|
|
@ -202,4 +202,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>New platform workloads continue to use <code>rail-kubernetes</code> unless a specialized rail is justified.</li><li>Derived rails declare base-rail compatibility rather than copying lifecycle contracts.</li><li>Fabric and conformance tooling must understand rail dependency and readiness.</li><li>Knative installation stays with the S2 substrate owner.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0005 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0005-derived-rail-composition.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
</section><footer><span>RMASTER-ADR-0005 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0005-derived-rail-composition.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="36ec3aad5082ceff685d66e091c0a24b52abfc65b36595d00dce2b52a7250f4e">
|
||||
<title>First-Wave reef Rollout</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0004</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-26</span><span>generated from canonical source — do not edit</span></div><h1>First-Wave reef Rollout</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0004-first-wave-reef-rollout.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-01-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0004</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-26</span><span>generated from canonical source — do not edit</span></div><h1>First-Wave reef Rollout</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0004-first-wave-reef-rollout.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-01-26</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Railiance now has a reef model, but it needs a concrete first rollout.</p>
|
||||
<p>The current substrate reality is not uniform:</p>
|
||||
|
|
@ -214,4 +214,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="notes"><h2>Notes</h2>
|
||||
<p>This ADR chooses the first rollout set. It does not require that every future substrate be modeled the same way.</p>
|
||||
</section><footer><span>RMASTER-ADR-0004 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0004-first-wave-reef-rollout.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
</section><footer><span>RMASTER-ADR-0004 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0004-first-wave-reef-rollout.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
211
build/adr/railiance-k3s-api-tunnel-only/v1/index.html
Normal file
211
build/adr/railiance-k3s-api-tunnel-only/v1/index.html
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="f3e8bf3ab4b1dafa4d0b251b88ab2ef23a30c37b">
|
||||
<meta name="policy-source-digest" content="4d93643d00d19b9d7c46843cd9be674c24e4cd613dc8e780c1684d97a6a9030a">
|
||||
<title>k3s API is tunnel-only</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RINFRA-ADR-0005</span> <span class="stat">accepted · accepted-1</span> <span>railiance-infra</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>k3s API is tunnel-only</h1><p class="sub">Source: <code>railiance-infra · docs/adr/ADR-005-k3s-api-tunnel-only.md · f3e8bf3ab4b1dafa4d0b251b88ab2ef23a30c37b</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-08-15 <strong>Deciders:</strong> implementation of RAIL-HO-WP-0009-T04 <strong>Workplans:</strong> <code>RAIL-HO-WP-0009</code></p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Operator addresses on this network rotate with the ISP lease. A public UFW allowlist for <code>6443/tcp</code> is therefore a treadmill:</p>
|
||||
<ul><li>miss a rotation and <code>kubectl</code> breaks mid-session</li><li>leave the old grant standing and it becomes a grant to a stranger</li><li>hand-add the new address and the declaration drifts again</li></ul>
|
||||
<p>That cycle produced this workplan. The live allowlist drifted by hand <em>during the session that was fixing allowlist drift</em>, and again before the next session (<code>89.244.90.248</code> appeared undeclared). On 2026-08-15 the workstation egress address was <code>85.132.220.102</code> — already on the revoked list as a "historic" operator address.</p>
|
||||
<p><code>docs/deploy-stack.md</code> already documents API access over the ops-bridge SSH tunnel for CoulombCore (<code>k3s-api-coulombcore</code>, local port 16443). The same tunnel already exists for Railiance01 (<code>k3s-api-railiance01</code>, local port 16444). SSH itself stays public, so the host remains recoverable.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>The public k3s API allowlist is <strong>empty</strong>. Operator and agent <code>kubectl</code> access uses the ops-bridge tunnels:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Cluster</th><th>Tunnel</th><th>Local port</th><th>Remote</th></tr></thead><tbody><tr><td>CoulombCore</td><td><code>k3s-api-coulombcore</code></td><td>16443</td><td>6443</td></tr><tr><td>Railiance01</td><td><code>k3s-api-railiance01</code></td><td>16444</td><td>6443</td></tr></tbody></table></div>
|
||||
<pre>bridge up k3s-api-railiance01
|
||||
# kubeconfig server: https://127.0.0.1:16444</pre>
|
||||
<p>Trade: every operator <code>kubectl</code> action depends on ops-bridge. That is accepted. A rotating public allowlist is the worse dependency.</p>
|
||||
<p>Emergency break-glass remains SSH: <code>ssh railiance01 -- sudo k3s kubectl …</code>. Do not re-open <code>6443/tcp</code> to Anywhere.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>k3s_api_allowed_sources</code> stays <code>[]</code>.</li><li>Former public grants live in <code>k3s_api_revoked_sources</code> so a firewall-tagged converge deletes them.</li><li>Goss asserts the 6443 allowlist size is exactly the declared length (zero) and that no revoked address remains.</li><li>Amending this ADR is required before adding any new public 6443 source.</li></ul>
|
||||
</section><footer><span>RINFRA-ADR-0005 · accepted-1 · accepted</span><span>railiance-infra · docs/adr/ADR-005-k3s-api-tunnel-only.md · f3e8bf3ab4b1dafa4d0b251b88ab2ef23a30c37b</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="f3e8bf3ab4b1dafa4d0b251b88ab2ef23a30c37b">
|
||||
<meta name="policy-source-digest" content="4d93643d00d19b9d7c46843cd9be674c24e4cd613dc8e780c1684d97a6a9030a">
|
||||
<title>k3s API is tunnel-only</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RINFRA-ADR-0005</span> <span class="stat">accepted · accepted-1</span> <span>railiance-infra</span> <span>reviewed 2026-08-22</span><span>generated from canonical source — do not edit</span></div><h1>k3s API is tunnel-only</h1><p class="sub">Source: <code>railiance-infra · docs/adr/ADR-005-k3s-api-tunnel-only.md · f3e8bf3ab4b1dafa4d0b251b88ab2ef23a30c37b</code></p><p class="sub">Review due: 2027-02-22</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p><strong>Status:</strong> Accepted <strong>Date:</strong> 2026-08-15 <strong>Deciders:</strong> implementation of RAIL-HO-WP-0009-T04 <strong>Workplans:</strong> <code>RAIL-HO-WP-0009</code></p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Operator addresses on this network rotate with the ISP lease. A public UFW allowlist for <code>6443/tcp</code> is therefore a treadmill:</p>
|
||||
<ul><li>miss a rotation and <code>kubectl</code> breaks mid-session</li><li>leave the old grant standing and it becomes a grant to a stranger</li><li>hand-add the new address and the declaration drifts again</li></ul>
|
||||
<p>That cycle produced this workplan. The live allowlist drifted by hand <em>during the session that was fixing allowlist drift</em>, and again before the next session (<code>89.244.90.248</code> appeared undeclared). On 2026-08-15 the workstation egress address was <code>85.132.220.102</code> — already on the revoked list as a "historic" operator address.</p>
|
||||
<p><code>docs/deploy-stack.md</code> already documents API access over the ops-bridge SSH tunnel for CoulombCore (<code>k3s-api-coulombcore</code>, local port 16443). The same tunnel already exists for Railiance01 (<code>k3s-api-railiance01</code>, local port 16444). SSH itself stays public, so the host remains recoverable.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>The public k3s API allowlist is <strong>empty</strong>. Operator and agent <code>kubectl</code> access uses the ops-bridge tunnels:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Cluster</th><th>Tunnel</th><th>Local port</th><th>Remote</th></tr></thead><tbody><tr><td>CoulombCore</td><td><code>k3s-api-coulombcore</code></td><td>16443</td><td>6443</td></tr><tr><td>Railiance01</td><td><code>k3s-api-railiance01</code></td><td>16444</td><td>6443</td></tr></tbody></table></div>
|
||||
<pre>bridge up k3s-api-railiance01
|
||||
# kubeconfig server: https://127.0.0.1:16444</pre>
|
||||
<p>Trade: every operator <code>kubectl</code> action depends on ops-bridge. That is accepted. A rotating public allowlist is the worse dependency.</p>
|
||||
<p>Emergency break-glass remains SSH: <code>ssh railiance01 -- sudo k3s kubectl …</code>. Do not re-open <code>6443/tcp</code> to Anywhere.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>k3s_api_allowed_sources</code> stays <code>[]</code>.</li><li>Former public grants live in <code>k3s_api_revoked_sources</code> so a firewall-tagged converge deletes them.</li><li>Goss asserts the 6443 allowlist size is exactly the declared length (zero) and that no revoked address remains.</li><li>Amending this ADR is required before adding any new public 6443 source.</li></ul>
|
||||
</section><footer><span>RINFRA-ADR-0005 · accepted-1 · accepted</span><span>railiance-infra · docs/adr/ADR-005-k3s-api-tunnel-only.md · f3e8bf3ab4b1dafa4d0b251b88ab2ef23a30c37b</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="c2f81c9718715fc08ea7a2e3021d0d3159958ace23b60a16430020637b9f57f2">
|
||||
<title>NetKingdom Security-Layer Interaction Boundary</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0009</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-08-29</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom Security-Layer Interaction Boundary</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0009-netkingdom-security-layer-interaction.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-08-29 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>NetKingdom Security Layer Model v0.7 is accepted. Section 20 restates Railiance workload-operation definitions owned by this repository and states consumption rules every Railiance consumer of NetKingdom security owes. Companion v0.2 §9 is the operative form of the same boundary.</p>
|
||||
<p>This repository had declared the four axes and the workload coverage rule in its own voice, but had no machine-readable layer declaration, no recorded assent to §20, and no framework contract that bound rails, rapps, and reefs to those consumption rules. Admission (ADR-0006) and exposure (ADR-0008) were live and were not demarcated from authorization.</p>
|
||||
<p>Statute §20.4: an interaction boundary between two frameworks is owned by neither alone. Changes to §20 require this repository's assent for the axis definitions and <code>glas-harness</code> assent for the session and tool-policy seam.</p>
|
||||
<p>Ratified 2026-08-29 under <code>RMASTER-WP-0026</code>.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<ol><li><strong>This repository is Taxonomy of Railiance workload operations.</strong> The machine-readable declaration is <a href="../../layer.yaml"><code>layer.yaml</code></a>. It is not a NetKingdom §4 catalog row. It is not PEP-shaped. It holds no Tooling-layer client.</li></ol>
|
||||
<ol><li><strong>Statute §20.1 restates our definitions and does not author them.</strong> Workload, the four axes, and the rule that <code>rein-*</code> is not a fifth axis remain this repository's. NetKingdom may cite them; it may not redefine them without our assent.</li></ol>
|
||||
<ol><li><strong>Statute §20.2 is the consumption constitution</strong> for every Railiance consumer of NetKingdom security. The detailed contract is <a href="../netkingdom-security-consumption-contract.md"><code>docs/netkingdom-security-consumption-contract.md</code></a>.</li></ol>
|
||||
<ol><li><strong>Statute §20.3 remains unset.</strong> This repository will not imply a mapping of rails, rapps, reefs, or ownership onto Taxonomy, Tooling, Engine, or Staff. The five questions are tracked, unanswered, in <a href="../netkingdom-axis-layer-open-questions.md"><code>docs/netkingdom-axis-layer-open-questions.md</code></a>.</li></ol>
|
||||
<ol><li><strong>Admission, exposure, and authorization stay three questions.</strong> ADR-0006 answers whether a binding may run in production. ADR-0008 answers who may reach a listener we control. <code>access-engine</code> answers whether an actor may perform an action. <code>production-approved</code> and <code>exposure: public</code> are not authorization decisions.</li></ol>
|
||||
<ol><li><strong>Changes to this boundary</strong> require this repository's assent for the axis definitions. Changes that touch the glas-harness seam require <code>glas-harness</code> assent as well.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Rails, rapps, and reefs consume <code>access-engine</code>, <code>approval-engine</code>, <code>secrets-engine</code>, and <code>audit-core</code>. They do not grow local substitutes.</li><li>This repository does not host a PDP, an approval store, a credential plane, an evidence archive, or an actuation surface.</li><li>PEP stance maps belong in the repositories that cause protected side effects, inventoried in statute §13.1, not here.</li><li>Observation-in-production and automatic containment remain estate-wide zeros. Framework plans must not assume they exist.</li><li><code>gate-house</code> can cite this ADR as this repository's own-voice declaration and §20 assent, rather than a review note about us.</li></ul>
|
||||
</section>
|
||||
<section id="notes"><h2>Notes</h2>
|
||||
<p>This ADR does not amend ADR-0001 through ADR-0008. It adds the security consumption axis those records did not have to name.</p>
|
||||
</section><footer><span>RMASTER-ADR-0009 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0009-netkingdom-security-layer-interaction.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="51747ca793d830c42bd86ce0ca43e34db9e8a1c1">
|
||||
<meta name="policy-source-digest" content="c2f81c9718715fc08ea7a2e3021d0d3159958ace23b60a16430020637b9f57f2">
|
||||
<title>NetKingdom Security-Layer Interaction Boundary</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0009</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-08-29</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom Security-Layer Interaction Boundary</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0009-netkingdom-security-layer-interaction.md · 51747ca793d830c42bd86ce0ca43e34db9e8a1c1</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-08-29 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>NetKingdom Security Layer Model v0.7 is accepted. Section 20 restates Railiance workload-operation definitions owned by this repository and states consumption rules every Railiance consumer of NetKingdom security owes. Companion v0.2 §9 is the operative form of the same boundary.</p>
|
||||
<p>This repository had declared the four axes and the workload coverage rule in its own voice, but had no machine-readable layer declaration, no recorded assent to §20, and no framework contract that bound rails, rapps, and reefs to those consumption rules. Admission (ADR-0006) and exposure (ADR-0008) were live and were not demarcated from authorization.</p>
|
||||
<p>Statute §20.4: an interaction boundary between two frameworks is owned by neither alone. Changes to §20 require this repository's assent for the axis definitions and <code>glas-harness</code> assent for the session and tool-policy seam.</p>
|
||||
<p>Ratified 2026-08-29 under <code>RMASTER-WP-0026</code>.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<ol><li><strong>This repository is Taxonomy of Railiance workload operations.</strong> The machine-readable declaration is <a href="../../layer.yaml"><code>layer.yaml</code></a>. It is not a NetKingdom §4 catalog row. It is not PEP-shaped. It holds no Tooling-layer client.</li></ol>
|
||||
<ol><li><strong>Statute §20.1 restates our definitions and does not author them.</strong> Workload, the four axes, and the rule that <code>rein-*</code> is not a fifth axis remain this repository's. NetKingdom may cite them; it may not redefine them without our assent.</li></ol>
|
||||
<ol><li><strong>Statute §20.2 is the consumption constitution</strong> for every Railiance consumer of NetKingdom security. The detailed contract is <a href="../netkingdom-security-consumption-contract.md"><code>docs/netkingdom-security-consumption-contract.md</code></a>.</li></ol>
|
||||
<ol><li><strong>Statute §20.3 remains unset.</strong> This repository will not imply a mapping of rails, rapps, reefs, or ownership onto Taxonomy, Tooling, Engine, or Staff. The five questions are tracked, unanswered, in <a href="../netkingdom-axis-layer-open-questions.md"><code>docs/netkingdom-axis-layer-open-questions.md</code></a>.</li></ol>
|
||||
<ol><li><strong>Admission, exposure, and authorization stay three questions.</strong> ADR-0006 answers whether a binding may run in production. ADR-0008 answers who may reach a listener we control. <code>access-engine</code> answers whether an actor may perform an action. <code>production-approved</code> and <code>exposure: public</code> are not authorization decisions.</li></ol>
|
||||
<ol><li><strong>Changes to this boundary</strong> require this repository's assent for the axis definitions. Changes that touch the glas-harness seam require <code>glas-harness</code> assent as well.</li></ol>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Rails, rapps, and reefs consume <code>access-engine</code>, <code>approval-engine</code>, <code>secrets-engine</code>, and <code>audit-core</code>. They do not grow local substitutes.</li><li>This repository does not host a PDP, an approval store, a credential plane, an evidence archive, or an actuation surface.</li><li>PEP stance maps belong in the repositories that cause protected side effects, inventoried in statute §13.1, not here.</li><li>Observation-in-production and automatic containment remain estate-wide zeros. Framework plans must not assume they exist.</li><li><code>gate-house</code> can cite this ADR as this repository's own-voice declaration and §20 assent, rather than a review note about us.</li></ul>
|
||||
</section>
|
||||
<section id="notes"><h2>Notes</h2>
|
||||
<p>This ADR does not amend ADR-0001 through ADR-0008. It adds the security consumption axis those records did not have to name.</p>
|
||||
</section><footer><span>RMASTER-ADR-0009 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0009-netkingdom-security-layer-interaction.md · 51747ca793d830c42bd86ce0ca43e34db9e8a1c1</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="56d516e10cdad6691f254ec9cb11f11ea364f7de">
|
||||
<meta name="policy-source-revision" content="e5f3497337575e1fd85fe2bfde5b2183c690d94e">
|
||||
<meta name="policy-source-digest" content="cfc0ad202c2eeeefd963127ff1defa127c715c001ecc684ab8759733bd8fb9f8">
|
||||
<title>ADR-0002 — S3 owns the placement rule; the package repo owns the number</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RPLAT-ADR-0002</span> <span class="stat">proposed · 1.0</span> <span>railiance-platform</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0002 — S3 owns the placement rule; the package repo owns the number</h1><p class="sub">Source: <code>railiance-platform · docs/adr/ADR-0002-placement-policy-ownership.md · 56d516e10cdad6691f254ec9cb11f11ea364f7de</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li></ol></nav><main><section id="context"><h2>Context</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RPLAT-ADR-0002</span> <span class="stat">proposed · 1.0</span> <span>railiance-platform</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0002 — S3 owns the placement rule; the package repo owns the number</h1><p class="sub">Source: <code>railiance-platform · docs/adr/ADR-0002-placement-policy-ownership.md · e5f3497337575e1fd85fe2bfde5b2183c690d94e</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li></ol></nav><main><section id="context"><h2>Context</h2>
|
||||
<p>An earlier draft of <code>net-kingdom/canon/standards/tenancy-posture_v0.1.md</code> §8.2 proposed that database placement policy — dedicated versus shared, and when that changes — be owned by <code>railiance-platform</code>, co-signed by <code>adaptive-pricing</code>. <code>tenant-engine</code> raised the same gap independently on 2026-08-16: both patterns are live on railiance01, neither is written down, and each new service copies whichever neighbour it looked at.</p>
|
||||
<p>The complication is that this repo no longer holds the specs. <code>RAILIANCE-WP-0012</code> and <code>RAILIANCE-WP-0015</code> moved the deployable surface to the <code>rapp-*</code> repos. <code>platform-pg</code>'s <code>instances</code>, <code>max_connections</code>, memory limit and retention are <code>rapp-postgres</code>'s cluster CR. Tenancy Posture §19.8 nonetheless asks <em>this repo</em> for <code>platform-pg</code>'s declared maximum size — a question one hop from where its answer lives.</p>
|
||||
<p>Accepting ownership without stating this would produce either an answer we cannot substantiate or a quiet non-answer.</p>
|
||||
|
|
@ -209,4 +209,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<section id="alternatives-considered"><h2>Alternatives considered</h2>
|
||||
<p><strong>Decline ownership; route it to <code>rapp-postgres</code>.</strong> They hold the specs and the operational knowledge. Rejected: placement is a cross-cluster question and <code>rapp-postgres</code> owns one package. A policy owned by one substrate's operator cannot govern movement between substrates.</p>
|
||||
<p><strong>Accept whole, including the numbers.</strong> Rejected: it would either re-import the deployable surface this repo deliberately gave up, or produce numbers restated here that drift from the CR — a second source of truth for exactly the values a consumer must be able to trust.</p>
|
||||
</section><footer><span>RPLAT-ADR-0002 · 1.0 · proposed</span><span>railiance-platform · docs/adr/ADR-0002-placement-policy-ownership.md · 56d516e10cdad6691f254ec9cb11f11ea364f7de</span></footer></main></div></div></html>
|
||||
</section><footer><span>RPLAT-ADR-0002 · 1.0 · proposed</span><span>railiance-platform · docs/adr/ADR-0002-placement-policy-ownership.md · e5f3497337575e1fd85fe2bfde5b2183c690d94e</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-digest" content="276ea38233413f1e23670bbf57c486abc361b0a275ca67efea7d677103713b32">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="bd97ec8ee663bb416fae53c8cfe29c7066e4fd3a51a5bc364895297e6eb994b7">
|
||||
<title>Private-by-default Exposure</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0008</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-08-15</span><span>generated from canonical source — do not edit</span></div><h1>Private-by-default Exposure</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0008-private-by-default-exposure.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-02-15</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-08-15 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0008</span> <span class="stat">accepted · accepted-2</span> <span>railiance-master</span> <span>reviewed 2026-08-29</span><span>generated from canonical source — do not edit</span></div><h1>Private-by-default Exposure</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0008-private-by-default-exposure.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-08-15 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0006 says a topology binding is not permission to run a workload in production. It does not say who may reach a listener. A working deploy, a <code>hosts_rail</code> / <code>binds_rapp</code> line, or an Ingress object has been enough to put something on the public internet.</p>
|
||||
<p>Family readiness vocabularies are deliberately not unified (<code>schemas/README.md</code>). Reef <code>lifecycle_state</code> has no <code>production-approved</code>. Rapp <code>readiness_state</code> has no <code>production-approved</code> either. Exposure cannot be derived from those enums.</p>
|
||||
|
|
@ -214,5 +214,5 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<p>The detailed per-family reading lives in <a href="../exposure-posture-contract.md"><code>docs/exposure-posture-contract.md</code></a>.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>ADR-0006 still answers "may this binding run in production?" This ADR answers "who may reach the listener?" Do not merge the axes.</li><li>The three readiness enums stay distinct on purpose.</li><li>Family schemas grow an additive <code>exposure</code> field. Rapp <code>data_classification: public</code> is a different field and must not be reused as the posture name.</li><li>Implementation stays in the owning repos. This ADR does not install NetworkPolicy, UFW, Ingress, or tunnels.</li><li>Existing public surfaces on <code>reef-railiance</code> remain up until named as grants. This ADR is not a shutdown plan.</li><li>CoulombCore host inventory and Q7 / Goss reaction stay outside this decision.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0008 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0008-private-by-default-exposure.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
<ul><li>ADR-0006 still answers "may this binding run in production?" This ADR answers "who may reach the listener?" Do not merge the axes.</li><li>Neither question is an authorization decision. Whether an actor may perform an action is <code>access-engine</code> (<a href="ADR-0009-netkingdom-security-layer-interaction.md"><code>ADR-0009</code></a>). <code>exposure: public</code> MUST NOT be read as permission to act.</li><li>The three readiness enums stay distinct on purpose.</li><li>Family schemas grow an additive <code>exposure</code> field. Rapp <code>data_classification: public</code> is a different field and must not be reused as the posture name.</li><li>Implementation stays in the owning repos. This ADR does not install NetworkPolicy, UFW, Ingress, or tunnels.</li><li>Existing public surfaces on <code>reef-railiance</code> remain up until named as grants. This ADR is not a shutdown plan.</li><li>CoulombCore host inventory and Q7 / Goss reaction stay outside this decision.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0008 · accepted-2 · accepted</span><span>railiance-master · docs/adr/ADR-0008-private-by-default-exposure.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="bd97ec8ee663bb416fae53c8cfe29c7066e4fd3a51a5bc364895297e6eb994b7">
|
||||
<title>Private-by-default Exposure</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0008</span> <span class="stat">accepted · accepted-2</span> <span>railiance-master</span> <span>reviewed 2026-08-29</span><span>generated from canonical source — do not edit</span></div><h1>Private-by-default Exposure</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0008-private-by-default-exposure.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-08-15 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>ADR-0006 says a topology binding is not permission to run a workload in production. It does not say who may reach a listener. A working deploy, a <code>hosts_rail</code> / <code>binds_rapp</code> line, or an Ingress object has been enough to put something on the public internet.</p>
|
||||
<p>Family readiness vocabularies are deliberately not unified (<code>schemas/README.md</code>). Reef <code>lifecycle_state</code> has no <code>production-approved</code>. Rapp <code>readiness_state</code> has no <code>production-approved</code> either. Exposure cannot be derived from those enums.</p>
|
||||
<p>Live public surfaces already exist on <code>reef-railiance</code>. They must be named as grants, not silently grandfathered and not taken down by this decision.</p>
|
||||
<p>Ratified 2026-08-15 under <code>RMASTER-WP-0023-T01</code>.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>New reefs, rails, and rapps are unreachable from the public internet until they are production-safe <strong>and</strong> explicitly granted. The field name is <code>exposure</code>. Missing field means <code>private</code>.</p>
|
||||
<div class="scroll"><table><thead><tr><th>Posture</th><th>Meaning</th><th>Who may reach it</th></tr></thead><tbody><tr><td><code>private</code></td><td>No extra listener we control</td><td>in-cluster DNS, same-reef services</td></tr><tr><td><code>operator</code></td><td>Same listener as <code>private</code>, plus a named ops-bridge / SSH tunnel</td><td>operator and approved agents</td></tr><tr><td><code>public</code></td><td>Internet listener we publish</td><td>anyone the Ingress / DNS / UFW surface allows</td></tr></tbody></table></div>
|
||||
<p><code>operator</code> is an access annotation, not a different packet posture. It does not open a host port or a public Ingress.</p>
|
||||
<p>Default for a new reef, rail, or rapp is <code>private</code>. Use <code>operator</code> only for a named admin or API surface that must be reachable before admission (k3s API, OpenBao UI). Do not prefer <code>operator</code> as the debug default.</p>
|
||||
<p><code>public</code> requires both an ADR-0006 binding of <code>production-approved</code> and an explicit grant. A deploy, a binding line, or an Ingress object is not a grant. Do not key <code>public</code> off rapp <code>readiness_state</code> and do not add <code>production-approved</code> to the rapp enum for this purpose.</p>
|
||||
<p>A reef public surface (host port or public DNS we publish) is a <strong>substrate grant</strong>, not reef <code>lifecycle_state</code>. A rapp cannot be <code>public</code> on a reef that has not granted a public surface. Conflicting declarations fail closed.</p>
|
||||
<p>The shared vocabulary is one enum. It is not one schema shape and not one admission check:</p>
|
||||
<div class="scroll"><table><thead><tr><th>Family</th><th>What the field means</th><th>What makes <code>public</code> legal</th></tr></thead><tbody><tr><td><code>rapp-*</code></td><td>intended consumer-facing listener</td><td>binding <code>production-approved</code> + grant</td></tr><tr><td><code>rail-*</code></td><td>which listener classes the rail may emit</td><td>rail may emit public Ingress only when a bound grant exists</td></tr><tr><td><code>reef-*</code></td><td>host ports and public DNS we publish</td><td>substrate grant</td></tr></tbody></table></div>
|
||||
<p>A grant is accepted residual risk, not a self-serve wish. Required fields: hostname or port, reason, <code>approved_on</code>, residual-risk owner.</p>
|
||||
<p><code>6443</code> / the k3s API is not a grantable public surface.</p>
|
||||
<p>This contract covers listeners we control: host ports, Ingress, Service types, and public DNS we publish. Provider-native internet APIs (Scaleway S3 and other provider-delegated endpoints) are outside this enum.</p>
|
||||
<p>Ops-bridge is the normal path to a shielded thing.</p>
|
||||
<p>The detailed per-family reading lives in <a href="../exposure-posture-contract.md"><code>docs/exposure-posture-contract.md</code></a>.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>ADR-0006 still answers "may this binding run in production?" This ADR answers "who may reach the listener?" Do not merge the axes.</li><li>Neither question is an authorization decision. Whether an actor may perform an action is <code>access-engine</code> (<a href="ADR-0009-netkingdom-security-layer-interaction.md"><code>ADR-0009</code></a>). <code>exposure: public</code> MUST NOT be read as permission to act.</li><li>The three readiness enums stay distinct on purpose.</li><li>Family schemas grow an additive <code>exposure</code> field. Rapp <code>data_classification: public</code> is a different field and must not be reused as the posture name.</li><li>Implementation stays in the owning repos. This ADR does not install NetworkPolicy, UFW, Ingress, or tunnels.</li><li>Existing public surfaces on <code>reef-railiance</code> remain up until named as grants. This ADR is not a shutdown plan.</li><li>CoulombCore host inventory and Q7 / Goss reaction stay outside this decision.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0008 · accepted-2 · accepted</span><span>railiance-master · docs/adr/ADR-0008-private-by-default-exposure.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="7e1fc5aedd7294192d8702a22b9f205e5bae20793836fdc48c0c071d10d7ab9d">
|
||||
<title>Wave 1 rail-kubernetes Boundary</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0002</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-25</span><span>generated from canonical source — do not edit</span></div><h1>Wave 1 rail-kubernetes Boundary</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-01-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0002</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-25</span><span>generated from canonical source — do not edit</span></div><h1>Wave 1 rail-kubernetes Boundary</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-01-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Railiance wants <code>rail-*</code> repos to represent workload execution contracts rather than abstract naming ideas.</p>
|
||||
<p>Today, the concrete Kubernetes workload contract already exists, but it is embedded in <code>railiance-cluster</code>. That repo currently owns both:</p>
|
||||
|
|
@ -215,4 +215,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<section id="notes"><h2>Notes</h2>
|
||||
<p>This ADR does not require all current files to move immediately.</p>
|
||||
<p>It requires the ownership line to be explicit now, so practical repo separation can proceed without ambiguity.</p>
|
||||
</section><footer><span>RMASTER-ADR-0002 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
</section><footer><span>RMASTER-ADR-0002 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-digest" content="263431f88ba04d6ab9ab3b6c0d6a2bb08634bfe83dc0719350f0855d399c18b2">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="b1185388ded53188dc624fbda9bee517dd3ac80b35e881cd4a7962531bec52ff">
|
||||
<title>Rapp Declaration Contract</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0007</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-08-13</span><span>generated from canonical source — do not edit</span></div><h1>Rapp Declaration Contract</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0007-rapp-declaration-contract.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-02-13</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-08-13 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0007</span> <span class="stat">accepted · accepted-2</span> <span>railiance-master</span> <span>reviewed 2026-08-23</span><span>generated from canonical source — do not edit</span></div><h1>Rapp Declaration Contract</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0007-rapp-declaration-contract.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-02-23</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-08-13 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>RMASTER-WP-0017</code> through <code>RMASTER-WP-0019</code> established the four-axis model and materialized the first family repos. The model held up. Its enforcement did not.</p>
|
||||
<p>A 2026-08-11 survey by <code>railiance-platform</code> found that the three live <code>rapp.yaml</code> files were mutually unreadable: rollout, smoke, and rollback contracts used different shapes; metadata that both rails carry consistently appeared in only one rapp; <code>reef-railiance</code> <code>bound_rapps</code> listed <code>rapp-qonto</code> only, while <code>rapp-openbao</code> and <code>rapp-postgres</code> were already live on the same reef. <code>docs/repo-family-bootstrap-contract.md</code> named fields in prose and could not catch any of this.</p>
|
||||
|
|
@ -203,8 +203,11 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<ol><li><strong>Two cardinalities stay distinct.</strong> Repos to rapps is many-to-many: a repo may appear in the <code>composition.member_repos</code> of more than one rapp. Deployables to rapps is one-to-one: every running deployable has exactly one rapp that owns its rollout. The coverage question — <em>does every live deployable belong to exactly one rapp?</em> — is well-formed only if these stay distinct.</li></ol>
|
||||
<ol><li><strong>Granularity is grouped-by-bounded-context.</strong> One rapp per cohesive group that deploys, versions, and rolls back together, not one rapp per deployable. Grouping is legitimate only where members share rollout and rollback fate. A single-repo rapp is the one-member case of the same <code>composition</code> block, not a second shape.</li></ol>
|
||||
<ol><li><strong>The schema is normative.</strong> <code>schemas/rapp.schema.json</code>, <code>schemas/rail.schema.json</code>, and <code>schemas/reef.schema.json</code> define the shapes. Framework prose cites those files. It does not restate their fields. Reef <code>bound_rapps</code> is a derived projection of <code>rapp.bound_reefs</code>, not a hand-maintained registry.</li></ol>
|
||||
<ol><li><strong>Coverage includes every managed running deployable.</strong> Application, operational, and tooling runtimes participate in the same exactly-one-rapp invariant when they are installed, scheduled, or otherwise operated as a managed deployable. This includes a managed one-shot Job; it does not turn a human command or approval act into a workload. Human access, credential patterns, broker actions, one-off operational acts, and infrastructure resources that are not workloads retain their native actor, lane, activity, or resource identity.</li></ol>
|
||||
<p>A running deployable that predates rapp extraction is migration debt. Until an authoritative declaration claims it, workload-based controls report it as <code>unknown</code>; they do not infer a rapp from its repository, namespace, path, labels, or apparent owner. A subject explicitly established as not being a workload is <code>not-applicable</code>. <code>unknown</code> and <code>not-applicable</code> are different outcomes and omission must not collapse them.</p>
|
||||
<p>Railiance Master remains the sole owner of the normative rapp vocabulary and schemas. Consumer catalogs may store explicit references and integration owners may resolve them, but neither creates a parallel declaration surface or copies rapp metadata as another source of truth.</p>
|
||||
<p>The detailed shapes, including the single normative form of the rollout, smoke, and rollback contracts, live in the schema files and <code>schemas/README.md</code>.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Drift across family declarations fails in <code>tools/validate-family-declarations.py</code> instead of accumulating in prose.</li><li><code>railiance-platform</code> <code>RAILIANCE-WP-0015-T02</code> can converge <code>rapp-openbao</code> and <code>rapp-postgres</code> onto one shape. Migration belongs to the owning repos; this ADR does not move any declaration.</li><li><code>reef-railiance</code> must stop treating <code>bound_rapps: [rapp-qonto]</code> as source of truth. The list is already stale.</li><li>Three further <code>rapp-*</code> repos (<code>rapp-secrets-engine</code>, <code>rapp-tenant-engine</code>, <code>rapp-user-engine</code>) carry the family prefix and no declaration. They are visible to the validator as undeclared and must be declared, renamed, or retired by their owners.</li><li>Calling the validator from <code>fix-consistency</code> still waits on <code>the-custodian</code> admitting the family prefixes into the classification standard. That sequencing is not this repo's.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0007 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0007-rapp-declaration-contract.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
<ul><li>Drift across family declarations fails in <code>tools/validate-family-declarations.py</code> instead of accumulating in prose.</li><li><code>railiance-platform</code> <code>RAILIANCE-WP-0015-T02</code> can converge <code>rapp-openbao</code> and <code>rapp-postgres</code> onto one shape. Migration belongs to the owning repos; this ADR does not move any declaration.</li><li><code>reef-railiance</code> must stop treating <code>bound_rapps: [rapp-qonto]</code> as source of truth. The list is already stale.</li><li>Three further <code>rapp-*</code> repos (<code>rapp-secrets-engine</code>, <code>rapp-tenant-engine</code>, <code>rapp-user-engine</code>) carry the family prefix and no declaration. They are visible to the validator as undeclared and must be declared, renamed, or retired by their owners.</li><li>Operational and tooling deployables are not exempt from family coverage. Existing pre-rapp runtimes may continue during migration, but their workload identity remains visibly <code>unknown</code> to controls until declared.</li><li>Runtime inventory is still required to prove universal coverage. Repository discovery alone cannot establish that every running unit has exactly one authoritative rapp.</li><li>Calling the validator from <code>fix-consistency</code> still waits on <code>the-custodian</code> admitting the family prefixes into the classification standard. That sequencing is not this repo's.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0007 · accepted-2 · accepted</span><span>railiance-master · docs/adr/ADR-0007-rapp-declaration-contract.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,213 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="51747ca793d830c42bd86ce0ca43e34db9e8a1c1">
|
||||
<meta name="policy-source-digest" content="b1185388ded53188dc624fbda9bee517dd3ac80b35e881cd4a7962531bec52ff">
|
||||
<title>Rapp Declaration Contract</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0007</span> <span class="stat">accepted · accepted-2</span> <span>railiance-master</span> <span>reviewed 2026-08-23</span><span>generated from canonical source — do not edit</span></div><h1>Rapp Declaration Contract</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0007-rapp-declaration-contract.md · 51747ca793d830c42bd86ce0ca43e34db9e8a1c1</code></p><p class="sub">Review due: 2027-02-23</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-08-13 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p><code>RMASTER-WP-0017</code> through <code>RMASTER-WP-0019</code> established the four-axis model and materialized the first family repos. The model held up. Its enforcement did not.</p>
|
||||
<p>A 2026-08-11 survey by <code>railiance-platform</code> found that the three live <code>rapp.yaml</code> files were mutually unreadable: rollout, smoke, and rollback contracts used different shapes; metadata that both rails carry consistently appeared in only one rapp; <code>reef-railiance</code> <code>bound_rapps</code> listed <code>rapp-qonto</code> only, while <code>rapp-openbao</code> and <code>rapp-postgres</code> were already live on the same reef. <code>docs/repo-family-bootstrap-contract.md</code> named fields in prose and could not catch any of this.</p>
|
||||
<p>The same survey treated rapp grouping as something that might be derived from Forgejo organizations or from State Hub domains. Neither works. A repo lives in exactly one Forgejo org, so <code>org:repo</code> is one-to-many. A repo may legitimately contribute to more than one rapp, so <code>rapp:repo</code> is many-to-many. A many-to-many grouping cannot be derived from a one-to-many one. Domains fail in both directions. The three dimensions also change at different speeds.</p>
|
||||
<p>Canon OAS P1 requires independent perspectives to be separate orthogonal dimensions. Decision <code>d07ee5f9</code> already recorded this from the <code>railiance-platform</code> side; this ADR is the framework record.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<ol><li><strong>Bounded rapp context is its own dimension.</strong> It is derived neither from Forgejo organizations nor from State Hub domains. Grouping is a first-class declaration, not a projection of some other axis.</li></ol>
|
||||
<ol><li><strong>Two cardinalities stay distinct.</strong> Repos to rapps is many-to-many: a repo may appear in the <code>composition.member_repos</code> of more than one rapp. Deployables to rapps is one-to-one: every running deployable has exactly one rapp that owns its rollout. The coverage question — <em>does every live deployable belong to exactly one rapp?</em> — is well-formed only if these stay distinct.</li></ol>
|
||||
<ol><li><strong>Granularity is grouped-by-bounded-context.</strong> One rapp per cohesive group that deploys, versions, and rolls back together, not one rapp per deployable. Grouping is legitimate only where members share rollout and rollback fate. A single-repo rapp is the one-member case of the same <code>composition</code> block, not a second shape.</li></ol>
|
||||
<ol><li><strong>The schema is normative.</strong> <code>schemas/rapp.schema.json</code>, <code>schemas/rail.schema.json</code>, and <code>schemas/reef.schema.json</code> define the shapes. Framework prose cites those files. It does not restate their fields. Reef <code>bound_rapps</code> is a derived projection of <code>rapp.bound_reefs</code>, not a hand-maintained registry.</li></ol>
|
||||
<ol><li><strong>Coverage includes every managed running deployable.</strong> Application, operational, and tooling runtimes participate in the same exactly-one-rapp invariant when they are installed, scheduled, or otherwise operated as a managed deployable. This includes a managed one-shot Job; it does not turn a human command or approval act into a workload. Human access, credential patterns, broker actions, one-off operational acts, and infrastructure resources that are not workloads retain their native actor, lane, activity, or resource identity.</li></ol>
|
||||
<p>A running deployable that predates rapp extraction is migration debt. Until an authoritative declaration claims it, workload-based controls report it as <code>unknown</code>; they do not infer a rapp from its repository, namespace, path, labels, or apparent owner. A subject explicitly established as not being a workload is <code>not-applicable</code>. <code>unknown</code> and <code>not-applicable</code> are different outcomes and omission must not collapse them.</p>
|
||||
<p>Railiance Master remains the sole owner of the normative rapp vocabulary and schemas. Consumer catalogs may store explicit references and integration owners may resolve them, but neither creates a parallel declaration surface or copies rapp metadata as another source of truth.</p>
|
||||
<p>The detailed shapes, including the single normative form of the rollout, smoke, and rollback contracts, live in the schema files and <code>schemas/README.md</code>.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li>Drift across family declarations fails in <code>tools/validate-family-declarations.py</code> instead of accumulating in prose.</li><li><code>railiance-platform</code> <code>RAILIANCE-WP-0015-T02</code> can converge <code>rapp-openbao</code> and <code>rapp-postgres</code> onto one shape. Migration belongs to the owning repos; this ADR does not move any declaration.</li><li><code>reef-railiance</code> must stop treating <code>bound_rapps: [rapp-qonto]</code> as source of truth. The list is already stale.</li><li>Three further <code>rapp-*</code> repos (<code>rapp-secrets-engine</code>, <code>rapp-tenant-engine</code>, <code>rapp-user-engine</code>) carry the family prefix and no declaration. They are visible to the validator as undeclared and must be declared, renamed, or retired by their owners.</li><li>Operational and tooling deployables are not exempt from family coverage. Existing pre-rapp runtimes may continue during migration, but their workload identity remains visibly <code>unknown</code> to controls until declared.</li><li>Runtime inventory is still required to prove universal coverage. Repository discovery alone cannot establish that every running unit has exactly one authoritative rapp.</li><li>Calling the validator from <code>fix-consistency</code> still waits on <code>the-custodian</code> admitting the family prefixes into the classification standard. That sequencing is not this repo's.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0007 · accepted-2 · accepted</span><span>railiance-master · docs/adr/ADR-0007-rapp-declaration-contract.md · 51747ca793d830c42bd86ce0ca43e34db9e8a1c1</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="28135e94758b6935518d2e83458c1e607deeabb341879b605eef6529b3168cbc">
|
||||
<title>First-Wave rapp Selection</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0003</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-25</span><span>generated from canonical source — do not edit</span></div><h1>First-Wave rapp Selection</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0003-rapp-first-wave-selection.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-01-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0003</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-25</span><span>generated from canonical source — do not edit</span></div><h1>First-Wave rapp Selection</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0003-rapp-first-wave-selection.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-01-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Railiance wants <code>rapp-*</code> repos to represent managed workload packages rather than new ownership layers.</p>
|
||||
<p>The current workload surfaces already suggest several candidates:</p>
|
||||
|
|
@ -213,4 +213,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="notes"><h2>Notes</h2>
|
||||
<p>This ADR chooses sequence, not a mandatory destination for every workload in the ecosystem.</p>
|
||||
</section><footer><span>RMASTER-ADR-0003 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0003-rapp-first-wave-selection.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
</section><footer><span>RMASTER-ADR-0003 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0003-rapp-first-wave-selection.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-digest" content="d9fbd9d21d86e461334abc24060c39127f2158ba25317b3d3dc326c4f7eaf08c">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="9839d8a4a4617b745cfacab6a0fb72157e88cddf53f40226d8246a64864de533">
|
||||
<title>Reef Production Admission</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0006</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-08-15</span><span>generated from canonical source — do not edit</span></div><h1>Reef Production Admission</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0006-reef-production-admission.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-02-15</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-07-26 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0006</span> <span class="stat">accepted · accepted-2</span> <span>railiance-master</span> <span>reviewed 2026-08-29</span><span>generated from canonical source — do not edit</span></div><h1>Reef Production Admission</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0006-reef-production-admission.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-07-26 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Fabric topology can say that a reef hosts a rail or binds a workload, but that does not demonstrate capacity, isolation, recoverability, or approval for a critical internet-facing service.</p>
|
||||
</section>
|
||||
|
|
@ -200,7 +200,8 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<p>Critical workloads require machine-readable conformance evidence plus explicit acceptance of residual risks that cannot be automated. Mixed-rail reefs use defined split triggers.</p>
|
||||
<p>The detailed contract is <a href="../reef-production-readiness-contract.md"><code>docs/reef-production-readiness-contract.md</code></a>.</p>
|
||||
<p>Who may reach a listener is a different axis: <a href="ADR-0008-private-by-default-exposure.md"><code>ADR-0008</code></a>. Production admission does not imply a public surface. A public surface requires this admission <strong>and</strong> an exposure grant.</p>
|
||||
<p>Neither admission nor exposure is an authorization decision. Whether an actor may perform an action on a resource is <code>access-engine</code> (<a href="ADR-0009-netkingdom-security-layer-interaction.md"><code>ADR-0009</code></a>). <code>production-approved</code> MUST NOT be read as permission to act.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>hosts_rail</code> and <code>binds_rapp</code> no longer imply deployability.</li><li><code>reef-railiance</code> may host Knative in wave 2, but Qonto cannot be called production-approved solely from that declaration.</li><li>Repeated evidence collection should become functional automation.</li><li><code>production-approved</code> is not permission to publish a listener. See ADR-0008.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0006 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0006-reef-production-admission.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
<ul><li><code>hosts_rail</code> and <code>binds_rapp</code> no longer imply deployability.</li><li><code>reef-railiance</code> may host Knative in wave 2, but Qonto cannot be called production-approved solely from that declaration.</li><li>Repeated evidence collection should become functional automation.</li><li><code>production-approved</code> is not permission to publish a listener. See ADR-0008.</li><li><code>production-approved</code> is not an authorization decision. See ADR-0009.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0006 · accepted-2 · accepted</span><span>railiance-master · docs/adr/ADR-0006-reef-production-admission.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="9839d8a4a4617b745cfacab6a0fb72157e88cddf53f40226d8246a64864de533">
|
||||
<title>Reef Production Admission</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
||||
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
||||
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
||||
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
||||
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
||||
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
||||
--chip-fg:#F6F7F8;
|
||||
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
||||
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
||||
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
--measure:66ch;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
||||
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
||||
--rule:#2A3138; --rule-strong:#3B444D;
|
||||
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
||||
--clay:#D08A76; --clay-soft:#3A211B;
|
||||
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
||||
--chip-fg:#12161A;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:var(--paper); color:var(--ink);
|
||||
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
||||
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
||||
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
||||
|
||||
/* ---------- rail ---------- */
|
||||
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
||||
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
||||
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
||||
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
||||
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
||||
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
||||
|
||||
/* ---------- header ---------- */
|
||||
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
||||
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
||||
.eyebrow .stat{color:var(--clay)}
|
||||
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
||||
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
||||
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
||||
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
||||
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
section{margin-bottom:60px;scroll-margin-top:24px}
|
||||
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
||||
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
||||
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
||||
p{margin:0 0 15px;max-width:var(--measure)}
|
||||
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
||||
li{margin-bottom:7px}
|
||||
strong{font-weight:600}
|
||||
em{font-style:italic}
|
||||
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
||||
a{color:var(--brass)}
|
||||
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
||||
|
||||
/* ---------- devices ---------- */
|
||||
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
||||
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
||||
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
||||
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
||||
.hard p:last-child{margin-bottom:0}
|
||||
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
||||
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
||||
|
||||
/* ---------- tables ---------- */
|
||||
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
||||
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
||||
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
||||
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
||||
td:first-child{color:var(--ink);font-weight:600}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
||||
|
||||
/* ---------- ladders ---------- */
|
||||
.breakout{margin:34px 0 40px}
|
||||
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
||||
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
||||
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
||||
.ladders{display:grid;gap:26px}
|
||||
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
||||
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
||||
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
||||
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
||||
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
||||
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
||||
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
||||
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
||||
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
||||
.rung.na{opacity:.42}
|
||||
|
||||
/* ---------- matrix ---------- */
|
||||
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
||||
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
||||
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
||||
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
||||
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
||||
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
||||
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
||||
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
||||
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
||||
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
||||
.rlab{min-height:60px}
|
||||
.clab{padding-top:7px;min-height:22px}
|
||||
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
||||
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
||||
.mnote .k{display:flex;align-items:center;gap:7px}
|
||||
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
||||
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
||||
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
||||
@media (max-width:640px){
|
||||
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
||||
.mcell{min-height:52px;padding:4px}
|
||||
.pin{font-size:8px;padding:1px 3px}
|
||||
.rlab{min-height:52px}
|
||||
}
|
||||
|
||||
/* ---------- methodology ---------- */
|
||||
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
||||
.verb{background:var(--surface);padding:18px 18px 20px}
|
||||
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
||||
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
||||
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
||||
|
||||
/* ---------- questions ---------- */
|
||||
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
||||
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
||||
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
||||
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
||||
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
||||
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
||||
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
||||
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
||||
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
||||
|
||||
/* ---------- misc ---------- */
|
||||
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
||||
.numbers .v{color:var(--ink);font-weight:600}
|
||||
.numbers .k{color:var(--ink-3)}
|
||||
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
||||
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
||||
.alt:last-of-type{border-bottom:none}
|
||||
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
||||
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
||||
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
||||
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
.tm td,.tm th{text-align:center}
|
||||
.tm td:first-child,.tm th:first-child{text-align:left}
|
||||
.yes{color:var(--l4);font-weight:700}
|
||||
.no{color:var(--clay);font-weight:700}
|
||||
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
||||
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
||||
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
||||
.route{background:var(--surface);padding:16px 18px}
|
||||
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
||||
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
||||
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
||||
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0006</span> <span class="stat">accepted · accepted-2</span> <span>railiance-master</span> <span>reviewed 2026-08-29</span><span>generated from canonical source — do not edit</span></div><h1>Reef Production Admission</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0006-reef-production-admission.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-02-28</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li></ol></nav><main><p>Date: 2026-07-26 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Fabric topology can say that a reef hosts a rail or binds a workload, but that does not demonstrate capacity, isolation, recoverability, or approval for a critical internet-facing service.</p>
|
||||
</section>
|
||||
<section id="decision"><h2>Decision</h2>
|
||||
<p>Rail and <code>rapp</code> bindings use explicit readiness states: <code>declared</code>, <code>installed</code>, <code>verified</code>, <code>production-approved</code>, and <code>deprecated</code>.</p>
|
||||
<p>Critical workloads require machine-readable conformance evidence plus explicit acceptance of residual risks that cannot be automated. Mixed-rail reefs use defined split triggers.</p>
|
||||
<p>The detailed contract is <a href="../reef-production-readiness-contract.md"><code>docs/reef-production-readiness-contract.md</code></a>.</p>
|
||||
<p>Who may reach a listener is a different axis: <a href="ADR-0008-private-by-default-exposure.md"><code>ADR-0008</code></a>. Production admission does not imply a public surface. A public surface requires this admission <strong>and</strong> an exposure grant.</p>
|
||||
<p>Neither admission nor exposure is an authorization decision. Whether an actor may perform an action on a resource is <code>access-engine</code> (<a href="ADR-0009-netkingdom-security-layer-interaction.md"><code>ADR-0009</code></a>). <code>production-approved</code> MUST NOT be read as permission to act.</p>
|
||||
</section>
|
||||
<section id="consequences"><h2>Consequences</h2>
|
||||
<ul><li><code>hosts_rail</code> and <code>binds_rapp</code> no longer imply deployability.</li><li><code>reef-railiance</code> may host Knative in wave 2, but Qonto cannot be called production-approved solely from that declaration.</li><li>Repeated evidence collection should become functional automation.</li><li><code>production-approved</code> is not permission to publish a listener. See ADR-0008.</li><li><code>production-approved</code> is not an authorization decision. See ADR-0009.</li></ul>
|
||||
</section><footer><span>RMASTER-ADR-0006 · accepted-2 · accepted</span><span>railiance-master · docs/adr/ADR-0006-reef-production-admission.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="468a52af2b14eba08e05be69c4d2866bfd8d9d7d">
|
||||
<meta name="policy-source-revision" content="5ffd7d1b40d56249f490a318e728047fd3517c4c">
|
||||
<meta name="policy-source-digest" content="b9c993ded8d79d6f871dba9cf08a320b2d619609a448ad3d02b632f5b6f76497">
|
||||
<title>Repository Prefix Architecture</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0001</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-25</span><span>generated from canonical source — do not edit</span></div><h1>Repository Prefix Architecture</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0001-repository-prefix-architecture.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</code></p><p class="sub">Review due: 2027-01-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#meaning-of-each-family"><span class="n">·</span>Meaning Of Each Family</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#current-interpretation-for-existing-repos"><span class="n">·</span>Current Interpretation For Existing Repos</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RMASTER-ADR-0001</span> <span class="stat">accepted · accepted-1</span> <span>railiance-master</span> <span>reviewed 2026-07-25</span><span>generated from canonical source — do not edit</span></div><h1>Repository Prefix Architecture</h1><p class="sub">Source: <code>railiance-master · docs/adr/ADR-0001-repository-prefix-architecture.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</code></p><p class="sub">Review due: 2027-01-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#meaning-of-each-family"><span class="n">·</span>Meaning Of Each Family</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#current-interpretation-for-existing-repos"><span class="n">·</span>Current Interpretation For Existing Repos</a></li><li><a href="#notes"><span class="n">·</span>Notes</a></li></ol></nav><main><p>Date: 2026-07-25 Status: Accepted</p>
|
||||
<section id="context"><h2>Context</h2>
|
||||
<p>Railiance already has a meaningful set of ownership repos such as <code>railiance-infra</code>, <code>railiance-cluster</code>, <code>railiance-platform</code>, <code>railiance-enablement</code>, <code>railiance-apps</code>, <code>railiance-forge</code>, and <code>railiance-fabric</code>.</p>
|
||||
<p>That structure is useful, but it does not by itself capture all of the dimensions Railiance now needs.</p>
|
||||
|
|
@ -232,4 +232,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
</section>
|
||||
<section id="notes"><h2>Notes</h2>
|
||||
<p>This ADR defines the repository taxonomy. It does not yet mandate a full migration or rename of existing repos. Migration should happen when it produces clearer ownership and lower ambiguity, not merely for naming purity.</p>
|
||||
</section><footer><span>RMASTER-ADR-0001 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0001-repository-prefix-architecture.md · 468a52af2b14eba08e05be69c4d2866bfd8d9d7d</span></footer></main></div></div></html>
|
||||
</section><footer><span>RMASTER-ADR-0001 · accepted-1 · accepted</span><span>railiance-master · docs/adr/ADR-0001-repository-prefix-architecture.md · 5ffd7d1b40d56249f490a318e728047fd3517c4c</span></footer></main></div></div></html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8">
|
||||
<meta name="policy-source-revision" content="56d516e10cdad6691f254ec9cb11f11ea364f7de">
|
||||
<meta name="policy-source-revision" content="e5f3497337575e1fd85fe2bfde5b2183c690d94e">
|
||||
<meta name="policy-source-digest" content="63697581401b53a8437835c2bb8b40f8054cc40d0bc7a2972f83a4a10377f6ba">
|
||||
<title>ADR-0001 — S3 owns platform services, not the substrate beneath them</title>
|
||||
<style>
|
||||
|
|
@ -191,7 +191,7 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
|
||||
</style>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RPLAT-ADR-0001</span> <span class="stat">accepted · 1.0</span> <span>railiance-platform</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0001 — S3 owns platform services, not the substrate beneath them</h1><p class="sub">Source: <code>railiance-platform · docs/adr/ADR-0001-s3-platform-service-boundary.md · 56d516e10cdad6691f254ec9cb11f11ea364f7de</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li></ol></nav><main><section id="context"><h2>Context</h2>
|
||||
<div class="wrap"><header><div class="eyebrow"><span>RPLAT-ADR-0001</span> <span class="stat">accepted · 1.0</span> <span>railiance-platform</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>ADR-0001 — S3 owns platform services, not the substrate beneath them</h1><p class="sub">Source: <code>railiance-platform · docs/adr/ADR-0001-s3-platform-service-boundary.md · e5f3497337575e1fd85fe2bfde5b2183c690d94e</code></p><p class="sub">Review due: 2027-02-17</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#alternatives-considered"><span class="n">·</span>Alternatives considered</a></li></ol></nav><main><section id="context"><h2>Context</h2>
|
||||
<p><code>railiance-platform</code> is S3 on the OAS Stack: the shared services several applications depend on — PostgreSQL, secrets, cache, object storage. The layers around it are S1 <code>railiance-infra</code> (OS and host concerns), S2 <code>railiance-cluster</code> (Kubernetes runtime, ingress), S4 <code>railiance-enablement</code> (tooling and CI), S5 <code>railiance-apps</code> (workloads).</p>
|
||||
<p>This boundary has been stated in <code>SCOPE.md</code> and in ADR-003 of <code>railiance-infra</code> since the five-repo split, and it has been tested twice. <code>RAIL-PL-WP-0001</code> existed to extract platform services <em>out</em> of S2 subcharts. On 2026-08-17 <code>POLICY-NEXUS-WP-0001</code> assigned this repo "the substrate — DNS, TLS, ingress, hosting" for <code>policy.coulomb.social</code>, which would move the boundary back the other way.</p>
|
||||
<p>The pressure is predictable and will recur: S3 is the layer that looks like it owns infrastructure, because it owns things that feel infrastructural. Recording the rule as an ADR rather than as a line in <code>SCOPE.md</code> gives future requests something to be answered against.</p>
|
||||
|
|
@ -206,4 +206,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
|
|||
<section id="alternatives-considered"><h2>Alternatives considered</h2>
|
||||
<p><strong>Accept the substrate assignment as written.</strong> Fastest, and the requester had already resolved it with the operator. Rejected: it re-imports the coupling <code>RAIL-PL-WP-0001</code> spent a workplan removing, and a boundary that yields to whoever asks most recently is not a boundary.</p>
|
||||
<p><strong>Own ingress for S3-adjacent services only.</strong> A narrower version, and it fails on the first argument about what counts as adjacent. The line has to be drawn where it can be checked.</p>
|
||||
</section><footer><span>RPLAT-ADR-0001 · 1.0 · accepted</span><span>railiance-platform · docs/adr/ADR-0001-s3-platform-service-boundary.md · 56d516e10cdad6691f254ec9cb11f11ea364f7de</span></footer></main></div></div></html>
|
||||
</section><footer><span>RPLAT-ADR-0001 · 1.0 · accepted</span><span>railiance-platform · docs/adr/ADR-0001-s3-platform-service-boundary.md · e5f3497337575e1fd85fe2bfde5b2183c690d94e</span></footer></main></div></div></html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue