Publish activity-core and ops-warden ADRs
Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 55s

Harvest ops-warden's five ready records. Register ACT-ADR-001–005 after
publication metadata landed in activity-core. Markitect arc42-v1 and
the estate infospace are now the validation and retrieval path.
This commit is contained in:
tegwick 2026-08-19 00:17:13 +02:00
parent 64b47d73b9
commit 7d608c0d57
36 changed files with 5850 additions and 128 deletions

View file

@ -0,0 +1,437 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="157a53907240733148137338c9826a56b77d04d4f41e59c6cb57e6b6f8d9534d">
<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-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>
<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: &quot;1.0&quot;
publisher: the-custodian/state-hub
governance: publisher-declared # publisher-declared | curated
status: active # active | deprecated | draft
introduced: &quot;2026-05-14&quot;
---
# 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
{
&quot;id&quot;: &quot;evt-7f3a1b2c&quot;,
&quot;type&quot;: &quot;org.repo.registered&quot;,
&quot;version&quot;: &quot;1.0&quot;,
&quot;timestamp&quot;: &quot;2026-05-14T10:00:00Z&quot;,
&quot;publisher&quot;: &quot;the-custodian/state-hub&quot;,
&quot;attributes&quot;: {
&quot;repo_slug&quot;: &quot;new-python-service&quot;,
&quot;domain&quot;: &quot;railiance&quot;,
&quot;tags&quot;: [&quot;python-service&quot;, &quot;fastapi&quot;],
&quot;registered_at&quot;: &quot;2026-05-14T10:00:00Z&quot;
}
}
```
## 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: &quot;1.0&quot;
status: active
trigger:
type: event # event | cron | scheduled
event_type: org.repo.registered # for type: event
# cron: &quot;0 9 * * 1&quot; # for type: cron (5-field, UTC)
# timezone: &quot;Europe/Berlin&quot; # optional, cron only
# misfire_policy: skip # skip | catchup | compress (cron only)
# at: &quot;2026-06-01T09:00:00Z&quot; # 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: &quot;2026-05-14&quot;
---
# 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: &#x27;&quot;python-service&quot; in event.attributes.tags&#x27;
action:
task_template: tasks/sbom-initial-scan.md
target_repo: event.attributes.repo_slug
priority: high
labels: [&quot;onboarding&quot;, &quot;security&quot;]
```
```rule
id: create-scope-generation
condition: &#x27;&quot;python-service&quot; in event.attributes.tags and context.repo_profile.scope_md_exists == false&#x27;
action:
task_template: tasks/generate-scope-md.md
target_repo: event.attributes.repo_slug
priority: medium
labels: [&quot;onboarding&quot;, &quot;documentation&quot;]
```
## 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: &#x27;event.attributes.domain != &quot;test_domain_v2&quot;&#x27;
trusted_fields:
- event.attributes.repo_slug
- event.attributes.domain
- event.attributes.tags
model: claude-sonnet-4-6
review_required: 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&#x27;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
[&quot;sbom&quot;, &quot;security&quot;, &quot;automated&quot;]
## 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-1 · accepted</span><span>activity-core · docs/adr/adr-002-definition-format.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,437 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="157a53907240733148137338c9826a56b77d04d4f41e59c6cb57e6b6f8d9534d">
<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-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>
<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: &quot;1.0&quot;
publisher: the-custodian/state-hub
governance: publisher-declared # publisher-declared | curated
status: active # active | deprecated | draft
introduced: &quot;2026-05-14&quot;
---
# 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
{
&quot;id&quot;: &quot;evt-7f3a1b2c&quot;,
&quot;type&quot;: &quot;org.repo.registered&quot;,
&quot;version&quot;: &quot;1.0&quot;,
&quot;timestamp&quot;: &quot;2026-05-14T10:00:00Z&quot;,
&quot;publisher&quot;: &quot;the-custodian/state-hub&quot;,
&quot;attributes&quot;: {
&quot;repo_slug&quot;: &quot;new-python-service&quot;,
&quot;domain&quot;: &quot;railiance&quot;,
&quot;tags&quot;: [&quot;python-service&quot;, &quot;fastapi&quot;],
&quot;registered_at&quot;: &quot;2026-05-14T10:00:00Z&quot;
}
}
```
## 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: &quot;1.0&quot;
status: active
trigger:
type: event # event | cron | scheduled
event_type: org.repo.registered # for type: event
# cron: &quot;0 9 * * 1&quot; # for type: cron (5-field, UTC)
# timezone: &quot;Europe/Berlin&quot; # optional, cron only
# misfire_policy: skip # skip | catchup | compress (cron only)
# at: &quot;2026-06-01T09:00:00Z&quot; # 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: &quot;2026-05-14&quot;
---
# 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: &#x27;&quot;python-service&quot; in event.attributes.tags&#x27;
action:
task_template: tasks/sbom-initial-scan.md
target_repo: event.attributes.repo_slug
priority: high
labels: [&quot;onboarding&quot;, &quot;security&quot;]
```
```rule
id: create-scope-generation
condition: &#x27;&quot;python-service&quot; in event.attributes.tags and context.repo_profile.scope_md_exists == false&#x27;
action:
task_template: tasks/generate-scope-md.md
target_repo: event.attributes.repo_slug
priority: medium
labels: [&quot;onboarding&quot;, &quot;documentation&quot;]
```
## 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: &#x27;event.attributes.domain != &quot;test_domain_v2&quot;&#x27;
trusted_fields:
- event.attributes.repo_slug
- event.attributes.domain
- event.attributes.tags
model: claude-sonnet-4-6
review_required: 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&#x27;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
[&quot;sbom&quot;, &quot;security&quot;, &quot;automated&quot;]
## 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-1 · accepted</span><span>activity-core · docs/adr/adr-002-definition-format.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,247 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="ac70015255b8972c7ee38f1a0fb934c6aa5f397634ddc298f0878a8eed6a774a">
<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-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>
<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>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>
<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-1 · accepted</span><span>activity-core · docs/adr/adr-001-event-bridge-architecture.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,247 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="ac70015255b8972c7ee38f1a0fb934c6aa5f397634ddc298f0878a8eed6a774a">
<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-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>
<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>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>
<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-1 · accepted</span><span>activity-core · docs/adr/adr-001-event-bridge-architecture.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,247 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="b9005e5f23dce53169e5f614ed461ce49e975266ff9a8fc01e2c6364fc5c91d3">
<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-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>
<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 “todays 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-cores <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>
</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 days fire.</li></ol>
<p>Claim API (sketch; implement in ACTIVITY-WP-0026):</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>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-1 · accepted</span><span>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,247 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="b9005e5f23dce53169e5f614ed461ce49e975266ff9a8fc01e2c6364fc5c91d3">
<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-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>
<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 “todays 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-cores <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>
</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 days fire.</li></ol>
<p>Claim API (sketch; implement in ACTIVITY-WP-0026):</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>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-1 · accepted</span><span>activity-core · docs/adr/adr-005-ops-runs-vs-dev-work-records.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,224 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="89b3a925d8cf6d9dbfe426980021b58281350201f316654b7fc1ee6554910ac6">
<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-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>
<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 89 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_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[&quot;known_candidates&quot;]</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-1 · accepted</span><span>activity-core · docs/adr/adr-004-producer-trust-boundary.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,224 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="89b3a925d8cf6d9dbfe426980021b58281350201f316654b7fc1ee6554910ac6">
<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-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>
<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 89 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_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[&quot;known_candidates&quot;]</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-1 · accepted</span><span>activity-core · docs/adr/adr-004-producer-trust-boundary.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,294 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="81ccfde427525f9a8d47f93c346813bc2d4003a990b503343e27c842a81a2ea6">
<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-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>
<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 == &quot;org.repo.registered&quot;</code></td></tr><tr><td>Comparison</td><td><code>&gt;</code>, <code>&lt;</code>, <code>&gt;=</code>, <code>&lt;=</code></td><td><code>event.attributes.sbom_age_days &gt; 30</code></td></tr><tr><td>Membership</td><td><code>in</code>, <code>not in</code></td><td><code>&quot;python-service&quot; 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) &gt; 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: &quot;Run SBOM rescan for {context.repo.repo_slug}&quot;
target_repo: context.repo.repo_slug
priority: medium
labels: [&quot;sbom&quot;, &quot;security&quot;, &quot;{context.repo.repo_slug}&quot;]
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: &#x27;context.repo.sbom_age_days &gt; 30&#x27;
action:
task_template: Run SBOM rescan for {context.repo.repo_slug}
target_repo: context.repo.repo_slug
priority: medium
labels: [&quot;sbom&quot;, &quot;security&quot;, &quot;automated&quot;]</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 &gt; 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: &#x27;{expression}&#x27; # 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_required: false # true | false — curator gate for output
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_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>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>&quot;rule&quot;</code></td><td><code>&quot;instruction&quot;</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>
<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>
<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) -&gt; bool</code> and <code>execute_instruction(instr: InstructionDef, event: EventEnvelope, context: dict) -&gt; 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-1 · accepted</span><span>activity-core · docs/adr/adr-003-rule-instruction-model.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -0,0 +1,294 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="41a3fb8b81bd521a5fa21af114975c54532df3ad">
<meta name="policy-source-digest" content="81ccfde427525f9a8d47f93c346813bc2d4003a990b503343e27c842a81a2ea6">
<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-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>
<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 == &quot;org.repo.registered&quot;</code></td></tr><tr><td>Comparison</td><td><code>&gt;</code>, <code>&lt;</code>, <code>&gt;=</code>, <code>&lt;=</code></td><td><code>event.attributes.sbom_age_days &gt; 30</code></td></tr><tr><td>Membership</td><td><code>in</code>, <code>not in</code></td><td><code>&quot;python-service&quot; 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) &gt; 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: &quot;Run SBOM rescan for {context.repo.repo_slug}&quot;
target_repo: context.repo.repo_slug
priority: medium
labels: [&quot;sbom&quot;, &quot;security&quot;, &quot;{context.repo.repo_slug}&quot;]
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: &#x27;context.repo.sbom_age_days &gt; 30&#x27;
action:
task_template: Run SBOM rescan for {context.repo.repo_slug}
target_repo: context.repo.repo_slug
priority: medium
labels: [&quot;sbom&quot;, &quot;security&quot;, &quot;automated&quot;]</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 &gt; 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: &#x27;{expression}&#x27; # 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_required: false # true | false — curator gate for output
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_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>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>&quot;rule&quot;</code></td><td><code>&quot;instruction&quot;</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>
<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>
<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) -&gt; bool</code> and <code>execute_instruction(instr: InstructionDef, event: EventEnvelope, context: dict) -&gt; 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-1 · accepted</span><span>activity-core · docs/adr/adr-003-rule-instruction-model.md · 41a3fb8b81bd521a5fa21af114975c54532df3ad</span></footer></main></div></div></html>

View file

@ -1,6 +1,6 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="b93928330ef7a76976fa62989bb54f47dbe04832">
<meta name="policy-source-revision" content="64b47d73b9fdf33df535db42c80db3a7435da5cf">
<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 · b93928330ef7a76976fa62989bb54f47dbe04832</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 · 64b47d73b9fdf33df535db42c80db3a7435da5cf</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>/&lt;kind&gt;/&lt;document&gt;/&lt;version&gt;/
@ -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 · b93928330ef7a76976fa62989bb54f47dbe04832</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 · 64b47d73b9fdf33df535db42c80db3a7435da5cf</span></footer></main></div></div></html>

View file

@ -0,0 +1,216 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="7df0bb364276e382cbee9383e7e67d399b0ac1b0353246e0ab323e629e732a6d">
<title>ADR-0001 — The routing catalog is a pointer layer, never a second copy</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-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>
<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>
<p><code>registry/routing/catalog.yaml</code> tells a worker which subsystem owns a credential need and where the authoritative procedure lives. The obvious temptation, every time someone uses it, is to add the procedure itself: the reader is already here, the steps are short, and one more copy seems cheaper than a second lookup.</p>
<p>That temptation is the failure mode. A copied procedure is correct on the day it is written and silently wrong afterwards, because the owner changes theirs without knowing ours exists. The estate has already paid for this once, between an ADR and its published page — fixed by generating the page from the markdown rather than maintaining both.</p>
<p>The catalog is consulted precisely when someone is about to touch a credential. Being confidently wrong there is worse than being absent.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>For any subsystem ops-warden does not own, a catalog entry carries identifiers and pointers only</strong><code>owner_repo</code>, <code>subsystem</code>, <code>wiki_ref</code>, <code>canon_ref</code>, <code>need_keywords</code>, and the secret-free handoff metadata <code>warden access</code> needs.</p>
<p><strong>Authored procedure is permitted only where <code>warden_executes: true</code>.</strong> A <code>steps:</code> block and a <code>cert_command:</code> may exist on the SSH certificate lane and nowhere else, because that is the one lane ops-warden actually owns. Rotation <code>steps:</code> are the narrow exception and describe what the <em>owner</em> does, recorded because rotation guidance had no other home; they are still pointers in spirit and must not grow into a runnable substitute for the owner's tooling.</p>
<p><strong>No secret material in this file, ever.</strong></p>
<p>This is enforced, not merely documented. <code>tests/test_routing.py</code> fails any non-SSH entry carrying a <code>steps</code> block, and checks that every <code>wiki_ref</code> anchor resolves to a real section. A rule that is only written down is a rule that erodes.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>Accepted cost.</strong> Two lookups instead of one. A worker who wants the procedure follows the pointer. We consider a correct second hop cheaper than a stale first one.</p>
<p><strong>Anchors must resolve.</strong> Because the entry is only a pointer, a broken pointer is a total failure rather than a cosmetic one. Hence the anchor test — which has already caught a real break (<code>ADHOC-2026-08-11-T01</code>, a stale <code>rapp-qonto-keycape-client</code> anchor).</p>
<p><strong>Other repos are bound by this.</strong> When another repo asks us to add or rename a lane, we add the pointer and decline to absorb the procedure. That has been exercised: on 2026-08-11 railiance-platform asked ops-warden to rename an active lane, and the answer was to cross-reference the id from their CCR rather than have this repo carry a second identity for the same thing.</p>
<p><strong>It constrains what this repo may usefully become.</strong> ops-warden cannot grow into a documentation site for other people's credential procedures, however often that is asked for. The value of the catalog is that a reader knows it points at truth rather than at a copy of truth.</p>
</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>

View file

@ -0,0 +1,216 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="7df0bb364276e382cbee9383e7e67d399b0ac1b0353246e0ab323e629e732a6d">
<title>ADR-0001 — The routing catalog is a pointer layer, never a second copy</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-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>
<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>
<p><code>registry/routing/catalog.yaml</code> tells a worker which subsystem owns a credential need and where the authoritative procedure lives. The obvious temptation, every time someone uses it, is to add the procedure itself: the reader is already here, the steps are short, and one more copy seems cheaper than a second lookup.</p>
<p>That temptation is the failure mode. A copied procedure is correct on the day it is written and silently wrong afterwards, because the owner changes theirs without knowing ours exists. The estate has already paid for this once, between an ADR and its published page — fixed by generating the page from the markdown rather than maintaining both.</p>
<p>The catalog is consulted precisely when someone is about to touch a credential. Being confidently wrong there is worse than being absent.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>For any subsystem ops-warden does not own, a catalog entry carries identifiers and pointers only</strong><code>owner_repo</code>, <code>subsystem</code>, <code>wiki_ref</code>, <code>canon_ref</code>, <code>need_keywords</code>, and the secret-free handoff metadata <code>warden access</code> needs.</p>
<p><strong>Authored procedure is permitted only where <code>warden_executes: true</code>.</strong> A <code>steps:</code> block and a <code>cert_command:</code> may exist on the SSH certificate lane and nowhere else, because that is the one lane ops-warden actually owns. Rotation <code>steps:</code> are the narrow exception and describe what the <em>owner</em> does, recorded because rotation guidance had no other home; they are still pointers in spirit and must not grow into a runnable substitute for the owner's tooling.</p>
<p><strong>No secret material in this file, ever.</strong></p>
<p>This is enforced, not merely documented. <code>tests/test_routing.py</code> fails any non-SSH entry carrying a <code>steps</code> block, and checks that every <code>wiki_ref</code> anchor resolves to a real section. A rule that is only written down is a rule that erodes.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>Accepted cost.</strong> Two lookups instead of one. A worker who wants the procedure follows the pointer. We consider a correct second hop cheaper than a stale first one.</p>
<p><strong>Anchors must resolve.</strong> Because the entry is only a pointer, a broken pointer is a total failure rather than a cosmetic one. Hence the anchor test — which has already caught a real break (<code>ADHOC-2026-08-11-T01</code>, a stale <code>rapp-qonto-keycape-client</code> anchor).</p>
<p><strong>Other repos are bound by this.</strong> When another repo asks us to add or rename a lane, we add the pointer and decline to absorb the procedure. That has been exercised: on 2026-08-11 railiance-platform asked ops-warden to rename an active lane, and the answer was to cross-reference the id from their CCR rather than have this repo carry a second identity for the same thing.</p>
<p><strong>It constrains what this repo may usefully become.</strong> ops-warden cannot grow into a documentation site for other people's credential procedures, however often that is asked for. The value of the catalog is that a reader knows it points at truth rather than at a copy of truth.</p>
</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>

View file

@ -0,0 +1,216 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="7dcc31732d774ddf2c98636b69ee12e2d74034836ed0def81b7e06461309b53a">
<title>ADR-0002 — ops-warden is a transparent conduit, never a secret broker</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-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>
<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>
<p><code>warden access</code> is the operator front door for every credential need in the estate. For lanes marked <code>exec_capable</code> it does more than advise: it runs the owner's tool and returns the value. Anything that fetches secrets on request looks like a broker, and the gravity toward becoming one is strong — a broker is more convenient at every individual call site.</p>
<p>The distinction is not stylistic. A broker holds authority; a conduit borrows the caller's. Only one of those creates a new thing worth attacking.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>ops-warden runs the owner's tool with the caller's own identity, and takes no custody of the value.</strong> The caller's credentials do the work. ops-warden holds nothing after the command returns, stores nothing, and caches nothing.</p>
<p><strong>Forbidden: a standing broker.</strong> ops-warden must not hold its own long-lived secret-read credential in order to serve values to callers who could not have fetched them themselves. If the caller lacks authority, the correct outcome is a denial from the owner's system — not a fetch performed on their behalf by a more privileged intermediary.</p>
<p>The test is a question: <em>could the caller have run this themselves?</em> If yes, we are a conduit and may proxy. If no, proxying is privilege laundering and is refused.</p>
<p><strong>Owner-native front doors outrank the proxy.</strong> Where an owner has shipped their own exec surface — <code>secrets-engine exec</code>, the railiance-platform credential broker — we route there and do not proxy. The proxy is a fallback for lanes nobody fronts yet, not a preferred path. This is why <code>whynot-design-npm-publish</code> and <code>ops-warden-warden-sign-token</code> are <code>native</code> rather than <code>interim</code>.</p>
<p><strong>The value must not land somewhere it will be logged.</strong> Sanctioned transports are <code>--out</code> (mode-0600 file), <code>--exec</code> (child process env), and <code>--wrap</code> (a single-use OpenBao wrapping token). Streaming to a non-terminal stdout is refused without an explicit <code>--unsafe-stdout</code>, which exists for interactive humans only.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>ops-warden never becomes a credential store, and gains no value by being compromised beyond the SSH CA it already holds.</strong> This is the whole point. An attacker who owns ops-warden gets the SSH signing lane — serious, bounded, and already the thing this repo is hardened around — not a key to every secret in the estate.</p>
<p><strong>Some requests cannot be served, and that is the correct answer.</strong> When a caller lacks authority, ops-warden routes and explains rather than fetching. This reads as unhelpfulness at the moment it happens; it is the property that makes the front door safe to point every agent at.</p>
<p><strong>Every proxied fetch is auditable and attributable to the caller</strong>, because it ran as them. <code>audit.jsonl</code> records metadata only — never values, guarded in code.</p>
<p><strong>The <code>--unsafe-stdout</code> escape hatch is a known liability.</strong> It exists because humans in terminals legitimately need to see values. It is also exactly the shape of the 2026-07-16 disclosure, where a value reached a captured stdout. <code>ADR-0004</code> constrains it further for agent sessions.</p>
</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>

View file

@ -0,0 +1,216 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="7dcc31732d774ddf2c98636b69ee12e2d74034836ed0def81b7e06461309b53a">
<title>ADR-0002 — ops-warden is a transparent conduit, never a secret broker</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-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>
<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>
<p><code>warden access</code> is the operator front door for every credential need in the estate. For lanes marked <code>exec_capable</code> it does more than advise: it runs the owner's tool and returns the value. Anything that fetches secrets on request looks like a broker, and the gravity toward becoming one is strong — a broker is more convenient at every individual call site.</p>
<p>The distinction is not stylistic. A broker holds authority; a conduit borrows the caller's. Only one of those creates a new thing worth attacking.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>ops-warden runs the owner's tool with the caller's own identity, and takes no custody of the value.</strong> The caller's credentials do the work. ops-warden holds nothing after the command returns, stores nothing, and caches nothing.</p>
<p><strong>Forbidden: a standing broker.</strong> ops-warden must not hold its own long-lived secret-read credential in order to serve values to callers who could not have fetched them themselves. If the caller lacks authority, the correct outcome is a denial from the owner's system — not a fetch performed on their behalf by a more privileged intermediary.</p>
<p>The test is a question: <em>could the caller have run this themselves?</em> If yes, we are a conduit and may proxy. If no, proxying is privilege laundering and is refused.</p>
<p><strong>Owner-native front doors outrank the proxy.</strong> Where an owner has shipped their own exec surface — <code>secrets-engine exec</code>, the railiance-platform credential broker — we route there and do not proxy. The proxy is a fallback for lanes nobody fronts yet, not a preferred path. This is why <code>whynot-design-npm-publish</code> and <code>ops-warden-warden-sign-token</code> are <code>native</code> rather than <code>interim</code>.</p>
<p><strong>The value must not land somewhere it will be logged.</strong> Sanctioned transports are <code>--out</code> (mode-0600 file), <code>--exec</code> (child process env), and <code>--wrap</code> (a single-use OpenBao wrapping token). Streaming to a non-terminal stdout is refused without an explicit <code>--unsafe-stdout</code>, which exists for interactive humans only.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>ops-warden never becomes a credential store, and gains no value by being compromised beyond the SSH CA it already holds.</strong> This is the whole point. An attacker who owns ops-warden gets the SSH signing lane — serious, bounded, and already the thing this repo is hardened around — not a key to every secret in the estate.</p>
<p><strong>Some requests cannot be served, and that is the correct answer.</strong> When a caller lacks authority, ops-warden routes and explains rather than fetching. This reads as unhelpfulness at the moment it happens; it is the property that makes the front door safe to point every agent at.</p>
<p><strong>Every proxied fetch is auditable and attributable to the caller</strong>, because it ran as them. <code>audit.jsonl</code> records metadata only — never values, guarded in code.</p>
<p><strong>The <code>--unsafe-stdout</code> escape hatch is a known liability.</strong> It exists because humans in terminals legitimately need to see values. It is also exactly the shape of the 2026-07-16 disclosure, where a value reached a captured stdout. <code>ADR-0004</code> constrains it further for agent sessions.</p>
</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>

View file

@ -0,0 +1,218 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="45b47c02afa575fbfe5be980e426c331a1d99bb9cd9c7a8de80bf8e319469f81">
<title>ADR-0003 — Cover gaps, but never silently own them</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-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>
<p>Accepted. Stated as INTENT §9, made structural by WARDEN-WP-0030 (delegation register).</p>
</section>
<section id="context"><h2>Context</h2>
<p>ops-warden owns exactly one lane: SSH certificate issuance. It nonetheless fronts around eleven credential lanes as a caller-identity proxy, because no other component fronts them yet and a worker blocked on a credential is a worker blocked.</p>
<p>Covering a gap is legitimate and this repo intends to keep doing it. The failure is subtler: <strong>a cover that is never recorded as a cover becomes ownership by default.</strong> Nobody decides to permanently own another component's lane. It happens because the interim arrangement worked, nobody wrote down that it was interim, and the intended owner never learned they were expected to build a front door.</p>
<p>By August 2026 the primitive to hand a lane back existed and was proven — <code>exec_owner</code> / <code>exec_command</code>, used by secrets-engine for npm publish and by the railiance-platform credential broker for warden-sign — and was used by 2 of 24 lanes. The other twenty-two had no record of who <em>should</em> own them.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>Every catalog entry carries a <code>delegation:</code> block</strong>, with a <code>mode:</code> of:</p>
<div class="scroll"><table><thead><tr><th><code>mode</code></th><th>Meaning</th></tr></thead><tbody><tr><td><code>permanent</code></td><td>Ours forever. SSH certificate issuance, and nothing else</td></tr><tr><td><code>native</code></td><td>The owner has a front door; we route to it and execute nothing</td></tr><tr><td><code>interim</code></td><td>We are covering a gap. Requires <code>intended_owner:</code> and <code>blocked_on:</code></td></tr></tbody></table></div>
<p><strong><code>interim</code> without an <code>intended_owner</code> is not permitted.</strong> If we cannot name who should own it, we have not understood the lane well enough to be fronting it.</p>
<p><strong><code>blocked_on:</code> must name a specific condition, not a mood.</strong> "No front door yet" is not a blocker; "secrets-engine has not confirmed whether <code>exec --catalog</code> generalizes over arbitrary OpenBao lanes (asked 2026-08-11, msg 7d55d332)" is. A blocker with a question and a date can be chased. A blocker without one is an excuse with a timestamp.</p>
<p><strong>The interim set is queryable</strong>: <code>warden route gaps</code> lists it with review dates and staleness. A cover that nobody can enumerate is a cover nobody will retire.</p>
<p><strong>A blocker is a claim about the world at a date, and expires.</strong> <code>reviewed:</code> is bumped only on a real re-check, never inherited. This was learned the hard way: <code>RISK-F-0001</code> invalidated one of our blockers within a day and nothing would have re-checked it.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>Retiring a cover is a normal, expected event rather than a renegotiation.</strong> When an owner ships their front door the lane flips <code>interim</code><code>native</code>. This has happened twice and both were routine.</p>
<p><strong>Other repos can see what we are holding for them.</strong> The register is why key-cape and user-engine were able to confirm or decline lanes in August 2026 — the question was answerable because it had been written down. One of those answers was "not ours", which is a legitimate and useful outcome.</p>
<p><strong>We accept looking worse than we are.</strong> <code>warden route gaps</code> publishes a list of things this repo is doing that it would rather not be doing. That is the intent: the alternative is a repo that looks clean because nobody counted.</p>
<p><strong>This register is not a risk register.</strong> Interim lanes are tracked work with an owner and a date, not defects. They do not get bulk-filed into <code>risk-nexus</code>, which needs to stay small enough to read. Defects go there; gaps stay here.</p>
</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>

View file

@ -0,0 +1,218 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="45b47c02afa575fbfe5be980e426c331a1d99bb9cd9c7a8de80bf8e319469f81">
<title>ADR-0003 — Cover gaps, but never silently own them</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-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>
<p>Accepted. Stated as INTENT §9, made structural by WARDEN-WP-0030 (delegation register).</p>
</section>
<section id="context"><h2>Context</h2>
<p>ops-warden owns exactly one lane: SSH certificate issuance. It nonetheless fronts around eleven credential lanes as a caller-identity proxy, because no other component fronts them yet and a worker blocked on a credential is a worker blocked.</p>
<p>Covering a gap is legitimate and this repo intends to keep doing it. The failure is subtler: <strong>a cover that is never recorded as a cover becomes ownership by default.</strong> Nobody decides to permanently own another component's lane. It happens because the interim arrangement worked, nobody wrote down that it was interim, and the intended owner never learned they were expected to build a front door.</p>
<p>By August 2026 the primitive to hand a lane back existed and was proven — <code>exec_owner</code> / <code>exec_command</code>, used by secrets-engine for npm publish and by the railiance-platform credential broker for warden-sign — and was used by 2 of 24 lanes. The other twenty-two had no record of who <em>should</em> own them.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>Every catalog entry carries a <code>delegation:</code> block</strong>, with a <code>mode:</code> of:</p>
<div class="scroll"><table><thead><tr><th><code>mode</code></th><th>Meaning</th></tr></thead><tbody><tr><td><code>permanent</code></td><td>Ours forever. SSH certificate issuance, and nothing else</td></tr><tr><td><code>native</code></td><td>The owner has a front door; we route to it and execute nothing</td></tr><tr><td><code>interim</code></td><td>We are covering a gap. Requires <code>intended_owner:</code> and <code>blocked_on:</code></td></tr></tbody></table></div>
<p><strong><code>interim</code> without an <code>intended_owner</code> is not permitted.</strong> If we cannot name who should own it, we have not understood the lane well enough to be fronting it.</p>
<p><strong><code>blocked_on:</code> must name a specific condition, not a mood.</strong> "No front door yet" is not a blocker; "secrets-engine has not confirmed whether <code>exec --catalog</code> generalizes over arbitrary OpenBao lanes (asked 2026-08-11, msg 7d55d332)" is. A blocker with a question and a date can be chased. A blocker without one is an excuse with a timestamp.</p>
<p><strong>The interim set is queryable</strong>: <code>warden route gaps</code> lists it with review dates and staleness. A cover that nobody can enumerate is a cover nobody will retire.</p>
<p><strong>A blocker is a claim about the world at a date, and expires.</strong> <code>reviewed:</code> is bumped only on a real re-check, never inherited. This was learned the hard way: <code>RISK-F-0001</code> invalidated one of our blockers within a day and nothing would have re-checked it.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>Retiring a cover is a normal, expected event rather than a renegotiation.</strong> When an owner ships their front door the lane flips <code>interim</code><code>native</code>. This has happened twice and both were routine.</p>
<p><strong>Other repos can see what we are holding for them.</strong> The register is why key-cape and user-engine were able to confirm or decline lanes in August 2026 — the question was answerable because it had been written down. One of those answers was "not ours", which is a legitimate and useful outcome.</p>
<p><strong>We accept looking worse than we are.</strong> <code>warden route gaps</code> publishes a list of things this repo is doing that it would rather not be doing. That is the intent: the alternative is a repo that looks clean because nobody counted.</p>
<p><strong>This register is not a risk register.</strong> Interim lanes are tracked work with an owner and a date, not defects. They do not get bulk-filed into <code>risk-nexus</code>, which needs to stay small enough to read. Defects go there; gaps stay here.</p>
</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>

View file

@ -0,0 +1,216 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="5db38dcb754af1ba639f4ceca056df842bc7b91fa48dd8bad21611aee29f682d">
<title>ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions</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-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>
<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>
<p>On 2026-07-16 a secret value reached a captured stdout. The mechanism was ordinary: <code>bao kv get -field=X</code> in an agent session. Nothing was misconfigured and nobody misused a tool. The value was read correctly, by an authorized caller, using the documented command — and an agent session records its stdout, so the value landed in a transcript that outlives the shell.</p>
<p>This is a structural mismatch, not a mistake to train away. Agent sessions are logged by design; that is what makes them reviewable. A human at a terminal sees a value and it scrolls away. An agent "seeing" a value writes it into a durable context that may be stored, replayed, or sent to an inference provider.</p>
<p>Guidance alone will not fix it. The command is correct, it is in every runbook, and the next agent that needs the value will reach for it.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>When <code>WARDEN_AGENT_ID</code> is set and the catalog lane is <code>risk: high</code>, ops-warden refuses to stream the raw value and exits 7.</strong> The agent is not blocked from doing its work — <code>--out</code>, <code>--exec</code>, <code>--wrap</code> and <code>--fingerprint</code> all remain available. It is blocked from doing its work <em>in a way that writes the secret into a transcript</em>.</p>
<p><strong>The boundary is enforced at the credential store as well as at the CLI.</strong> The OpenBao policy <code>agent-high-risk-boundary</code> denies data-read on those paths for agent tokens, allowing metadata and capabilities only. A control that lives solely in our own CLI is a control that ends the moment someone calls <code>bao</code> directly.</p>
<p><strong>Verification must not require a read.</strong> To check a lane, use <code>bao token capabilities</code> — allow/deny — never a read of the value. This is the specific habit the disclosure taught us to break.</p>
<p><strong>Exposure is reportable without reading.</strong> <code>warden taint &lt;catalog-id&gt;</code> reports KV v2 <code>custom_metadata</code> (<code>exposed_at</code>, <code>exposed_version</code>) and touches no secret data.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>Agents can still do everything they could before, by a different route.</strong> <code>--exec</code> covers nearly every real case: the child process gets the value in its environment, the agent never sees it. The friction is deliberate and small.</p>
<p><strong>Exit 7 is a contract other runtimes depend on.</strong> It is a distinguishable code, not a generic failure, so a caller can tell "refused by boundary" from "lane broken" and retry correctly. Changing it is a breaking change to every agent runtime.</p>
<p><strong><code>risk: high</code> becomes a load-bearing catalog field</strong> rather than documentation. Classifying a lane now changes runtime behaviour, so it must be set deliberately.</p>
<p><strong>We accept that <code>--unsafe-stdout</code> still exists for humans.</strong> The boundary keys on <code>WARDEN_AGENT_ID</code>, so an agent that does not set it is not caught. That is a known limit: this ADR raises the floor for cooperating runtimes and hardens the store behind them; it does not claim to stop a determined caller.</p>
</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>

View file

@ -0,0 +1,216 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="5db38dcb754af1ba639f4ceca056df842bc7b91fa48dd8bad21611aee29f682d">
<title>ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions</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-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>
<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>
<p>On 2026-07-16 a secret value reached a captured stdout. The mechanism was ordinary: <code>bao kv get -field=X</code> in an agent session. Nothing was misconfigured and nobody misused a tool. The value was read correctly, by an authorized caller, using the documented command — and an agent session records its stdout, so the value landed in a transcript that outlives the shell.</p>
<p>This is a structural mismatch, not a mistake to train away. Agent sessions are logged by design; that is what makes them reviewable. A human at a terminal sees a value and it scrolls away. An agent "seeing" a value writes it into a durable context that may be stored, replayed, or sent to an inference provider.</p>
<p>Guidance alone will not fix it. The command is correct, it is in every runbook, and the next agent that needs the value will reach for it.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>When <code>WARDEN_AGENT_ID</code> is set and the catalog lane is <code>risk: high</code>, ops-warden refuses to stream the raw value and exits 7.</strong> The agent is not blocked from doing its work — <code>--out</code>, <code>--exec</code>, <code>--wrap</code> and <code>--fingerprint</code> all remain available. It is blocked from doing its work <em>in a way that writes the secret into a transcript</em>.</p>
<p><strong>The boundary is enforced at the credential store as well as at the CLI.</strong> The OpenBao policy <code>agent-high-risk-boundary</code> denies data-read on those paths for agent tokens, allowing metadata and capabilities only. A control that lives solely in our own CLI is a control that ends the moment someone calls <code>bao</code> directly.</p>
<p><strong>Verification must not require a read.</strong> To check a lane, use <code>bao token capabilities</code> — allow/deny — never a read of the value. This is the specific habit the disclosure taught us to break.</p>
<p><strong>Exposure is reportable without reading.</strong> <code>warden taint &lt;catalog-id&gt;</code> reports KV v2 <code>custom_metadata</code> (<code>exposed_at</code>, <code>exposed_version</code>) and touches no secret data.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>Agents can still do everything they could before, by a different route.</strong> <code>--exec</code> covers nearly every real case: the child process gets the value in its environment, the agent never sees it. The friction is deliberate and small.</p>
<p><strong>Exit 7 is a contract other runtimes depend on.</strong> It is a distinguishable code, not a generic failure, so a caller can tell "refused by boundary" from "lane broken" and retry correctly. Changing it is a breaking change to every agent runtime.</p>
<p><strong><code>risk: high</code> becomes a load-bearing catalog field</strong> rather than documentation. Classifying a lane now changes runtime behaviour, so it must be set deliberately.</p>
<p><strong>We accept that <code>--unsafe-stdout</code> still exists for humans.</strong> The boundary keys on <code>WARDEN_AGENT_ID</code>, so an agent that does not set it is not caught. That is a known limit: this ADR raises the floor for cooperating runtimes and hardens the store behind them; it does not claim to stop a determined caller.</p>
</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>

View file

@ -0,0 +1,214 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="31eafe4d8d9362a3446739d63c3af83fd9138cfdc6ad120086312a67dc0d27d0">
<title>ADR-0005 — Implement one lane narrowly, route everything else</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-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>
<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>
<p>ops-warden began as an SSH certificate manager. It then became the place workers asked when they did not know where a credential came from — which is a real need, and the obvious way to serve it is to start fetching credentials.</p>
<p>Down that path is a component that issues SSH certificates, vends API keys, brokers tokens, and holds authority over all of them: a single point whose compromise is total. NetKingdom's architecture deliberately separates identity (key-cape), authorization (flex-auth), and secrets (OpenBao). A helpful front door that absorbed all three would quietly undo that separation, one convenience at a time.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>ops-warden executes exactly one lane with its own authority: SSH certificate issuance for <code>adm</code>/<code>agt</code>/<code>atm</code> actors.</strong> <code>warden_executes: true</code> appears on one catalog entry and is expected to stay that way.</p>
<p><strong>For every other need it routes, and where the lane is <code>exec_capable</code> it may assist by proxying as the caller</strong> under <code>ADR-0002</code>. Routing is not a lesser service — it is the service. Knowing which subsystem owns a need, and being right about it, is what this repo sells.</p>
<p><strong>Scope growth is tested by ownership, not by usefulness.</strong> "Would this be handy in ops-warden?" is the wrong question and almost always answers yes. The right question is "does ops-warden have the authority to own this, permanently?" If the answer is no, the correct outcome is a pointer, or an <code>interim</code> cover recorded under <code>ADR-0003</code>.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>The blast radius stays bounded and known.</strong> Compromising ops-warden yields the SSH signing lane. That is worth defending well precisely because it is the only thing here.</p>
<p><strong>We say no to requests that would be easy to say yes to.</strong> <code>warden secret</code>, <code>warden login</code>, <code>warden bao</code>, <code>warden tunnel</code> do not exist and must not be invented; the agent instructions name them as anti-patterns because agents keep reaching for them. Each would be a day's work and a permanent widening.</p>
<p><strong>Being useful therefore depends on the pointers being right</strong>, which is the whole weight behind <code>ADR-0001</code>'s anchor enforcement and the catalog's review dates. A router that routes wrongly is worse than no router.</p>
<p><strong>It leaves real gaps visible rather than filled.</strong> Six workload lanes and three tenant lanes are covered interim because secrets-engine and tenant-engine have not shipped front doors. Under this ADR that is the correct state, tracked under <code>ADR-0003</code>, and not a signal that ops-warden should absorb them.</p>
</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>

View file

@ -0,0 +1,214 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="35aff380a33f51a512c1e1b42d52d1dc0d95930f">
<meta name="policy-source-digest" content="31eafe4d8d9362a3446739d63c3af83fd9138cfdc6ad120086312a67dc0d27d0">
<title>ADR-0005 — Implement one lane narrowly, route everything else</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-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>
<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>
<p>ops-warden began as an SSH certificate manager. It then became the place workers asked when they did not know where a credential came from — which is a real need, and the obvious way to serve it is to start fetching credentials.</p>
<p>Down that path is a component that issues SSH certificates, vends API keys, brokers tokens, and holds authority over all of them: a single point whose compromise is total. NetKingdom's architecture deliberately separates identity (key-cape), authorization (flex-auth), and secrets (OpenBao). A helpful front door that absorbed all three would quietly undo that separation, one convenience at a time.</p>
</section>
<section id="decision"><h2>Decision</h2>
<p><strong>ops-warden executes exactly one lane with its own authority: SSH certificate issuance for <code>adm</code>/<code>agt</code>/<code>atm</code> actors.</strong> <code>warden_executes: true</code> appears on one catalog entry and is expected to stay that way.</p>
<p><strong>For every other need it routes, and where the lane is <code>exec_capable</code> it may assist by proxying as the caller</strong> under <code>ADR-0002</code>. Routing is not a lesser service — it is the service. Knowing which subsystem owns a need, and being right about it, is what this repo sells.</p>
<p><strong>Scope growth is tested by ownership, not by usefulness.</strong> "Would this be handy in ops-warden?" is the wrong question and almost always answers yes. The right question is "does ops-warden have the authority to own this, permanently?" If the answer is no, the correct outcome is a pointer, or an <code>interim</code> cover recorded under <code>ADR-0003</code>.</p>
</section>
<section id="consequences"><h2>Consequences</h2>
<p><strong>The blast radius stays bounded and known.</strong> Compromising ops-warden yields the SSH signing lane. That is worth defending well precisely because it is the only thing here.</p>
<p><strong>We say no to requests that would be easy to say yes to.</strong> <code>warden secret</code>, <code>warden login</code>, <code>warden bao</code>, <code>warden tunnel</code> do not exist and must not be invented; the agent instructions name them as anti-patterns because agents keep reaching for them. Each would be a day's work and a permanent widening.</p>
<p><strong>Being useful therefore depends on the pointers being right</strong>, which is the whole weight behind <code>ADR-0001</code>'s anchor enforcement and the catalog's review dates. A router that routes wrongly is worse than no router.</p>
<p><strong>It leaves real gaps visible rather than filled.</strong> Six workload lanes and three tenant lanes are covered interim because secrets-engine and tenant-engine have not shipped front doors. Under this ADR that is the correct state, tracked under <code>ADR-0003</code>, and not a signal that ops-warden should absorb them.</p>
</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>

View file

@ -1,6 +1,6 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="ba5d8642e91a31055eafdaef6993d779f6af7e06">
<meta name="policy-source-revision" content="0e0962e68b39fdcfb633b5058602174a36416a68">
<meta name="policy-source-digest" content="dd42250edcc444fa9a5007c3f9d561984840af2b19a07cf2916cbcc8434df0f7">
<title>NetKingdom 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>net-kingdom-architecture</span> <span class="stat">proposed · draft-1</span> <span>net-kingdom</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom architecture</h1><p class="sub">Source: <code>net-kingdom · docs/architecture/net-kingdom_v0.1.md · ba5d8642e91a31055eafdaef6993d779f6af7e06</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="#about-this-document"><span class="n">·</span>About this document</a></li><li><a href="#s1"><span class="n">1</span>Introduction and Goals</a></li><li><a href="#s2"><span class="n">2</span>Architecture Constraints</a></li><li><a href="#s3"><span class="n">3</span>System Scope and Context</a></li><li><a href="#s4"><span class="n">4</span>Solution Strategy</a></li><li><a href="#s5"><span class="n">5</span>Building Block View</a></li><li><a href="#s6"><span class="n">6</span>Runtime View</a></li><li><a href="#s7"><span class="n">7</span>Deployment View</a></li><li><a href="#s8"><span class="n">8</span>Cross-Cutting Concepts</a></li><li><a href="#s9"><span class="n">9</span>Architecture Decisions</a></li><li><a href="#s10"><span class="n">10</span>Quality Requirements</a></li><li><a href="#s11"><span class="n">11</span>Risks and Technical Debt</a></li><li><a href="#s12"><span class="n">12</span>Glossary</a></li></ol></nav><main><section id="about-this-document"><h2>About this document</h2>
<div class="wrap"><header><div class="eyebrow"><span>net-kingdom-architecture</span> <span class="stat">proposed · draft-1</span> <span>net-kingdom</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom architecture</h1><p class="sub">Source: <code>net-kingdom · docs/architecture/net-kingdom_v0.1.md · 0e0962e68b39fdcfb633b5058602174a36416a68</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="#about-this-document"><span class="n">·</span>About this document</a></li><li><a href="#s1"><span class="n">1</span>Introduction and Goals</a></li><li><a href="#s2"><span class="n">2</span>Architecture Constraints</a></li><li><a href="#s3"><span class="n">3</span>System Scope and Context</a></li><li><a href="#s4"><span class="n">4</span>Solution Strategy</a></li><li><a href="#s5"><span class="n">5</span>Building Block View</a></li><li><a href="#s6"><span class="n">6</span>Runtime View</a></li><li><a href="#s7"><span class="n">7</span>Deployment View</a></li><li><a href="#s8"><span class="n">8</span>Cross-Cutting Concepts</a></li><li><a href="#s9"><span class="n">9</span>Architecture Decisions</a></li><li><a href="#s10"><span class="n">10</span>Quality Requirements</a></li><li><a href="#s11"><span class="n">11</span>Risks and Technical Debt</a></li><li><a href="#s12"><span class="n">12</span>Glossary</a></li></ol></nav><main><section id="about-this-document"><h2>About this document</h2>
<p>First-wave arc42 for NetKingdom: the estate's identity and tenancy security core. Chapter 9 lists governing ADRs and standards; it does not paste them.</p>
</section>
<section id="s1"><h2><span class="sn">01</span>Introduction and Goals</h2>
@ -241,4 +241,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
</section>
<section id="s12"><h2><span class="sn">12</span>Glossary</h2>
<div class="scroll"><table><thead><tr><th>Term</th><th>Meaning</th></tr></thead><tbody><tr><td>IAM Profile</td><td>Provider-neutral OIDC contract owned here.</td></tr><tr><td>Tenancy Posture</td><td>Graduated axes for describing multi-tenancy.</td></tr><tr><td>Tenant-engine</td><td>Lifecycle and capability roles for tenants.</td></tr></tbody></table></div>
</section><footer><span>net-kingdom-architecture · draft-1 · proposed</span><span>net-kingdom · docs/architecture/net-kingdom_v0.1.md · ba5d8642e91a31055eafdaef6993d779f6af7e06</span></footer></main></div></div></html>
</section><footer><span>net-kingdom-architecture · draft-1 · proposed</span><span>net-kingdom · docs/architecture/net-kingdom_v0.1.md · 0e0962e68b39fdcfb633b5058602174a36416a68</span></footer></main></div></div></html>

View file

@ -1,6 +1,6 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="b93928330ef7a76976fa62989bb54f47dbe04832">
<meta name="policy-source-revision" content="64b47d73b9fdf33df535db42c80db3a7435da5cf">
<meta name="policy-source-digest" content="179cbb86bca95f71f46f51ca1971ff1c274eed7d900adf0672b537d4bcc5b480">
<title>Policy Nexus 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>policy-nexus-architecture</span> <span class="stat">proposed · draft-1</span> <span>the-custodian</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>Policy Nexus architecture</h1><p class="sub">Source: <code>policy-nexus · docs/architecture/policy-nexus_v0.1.md · b93928330ef7a76976fa62989bb54f47dbe04832</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="#about-this-document"><span class="n">·</span>About this document</a></li><li><a href="#s1"><span class="n">1</span>Introduction and Goals</a></li><li><a href="#s2"><span class="n">2</span>Architecture Constraints</a></li><li><a href="#s3"><span class="n">3</span>System Scope and Context</a></li><li><a href="#s4"><span class="n">4</span>Solution Strategy</a></li><li><a href="#s5"><span class="n">5</span>Building Block View</a></li><li><a href="#s6"><span class="n">6</span>Runtime View</a></li><li><a href="#s7"><span class="n">7</span>Deployment View</a></li><li><a href="#s8"><span class="n">8</span>Cross-Cutting Concepts</a></li><li><a href="#s9"><span class="n">9</span>Architecture Decisions</a></li><li><a href="#s10"><span class="n">10</span>Quality Requirements</a></li><li><a href="#s11"><span class="n">11</span>Risks and Technical Debt</a></li><li><a href="#s12"><span class="n">12</span>Glossary</a></li></ol></nav><main><section id="about-this-document"><h2>About this document</h2>
<div class="wrap"><header><div class="eyebrow"><span>policy-nexus-architecture</span> <span class="stat">proposed · draft-1</span> <span>the-custodian</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>Policy Nexus architecture</h1><p class="sub">Source: <code>policy-nexus · docs/architecture/policy-nexus_v0.1.md · 64b47d73b9fdf33df535db42c80db3a7435da5cf</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="#about-this-document"><span class="n">·</span>About this document</a></li><li><a href="#s1"><span class="n">1</span>Introduction and Goals</a></li><li><a href="#s2"><span class="n">2</span>Architecture Constraints</a></li><li><a href="#s3"><span class="n">3</span>System Scope and Context</a></li><li><a href="#s4"><span class="n">4</span>Solution Strategy</a></li><li><a href="#s5"><span class="n">5</span>Building Block View</a></li><li><a href="#s6"><span class="n">6</span>Runtime View</a></li><li><a href="#s7"><span class="n">7</span>Deployment View</a></li><li><a href="#s8"><span class="n">8</span>Cross-Cutting Concepts</a></li><li><a href="#s9"><span class="n">9</span>Architecture Decisions</a></li><li><a href="#s10"><span class="n">10</span>Quality Requirements</a></li><li><a href="#s11"><span class="n">11</span>Risks and Technical Debt</a></li><li><a href="#s12"><span class="n">12</span>Glossary</a></li></ol></nav><main><section id="about-this-document"><h2>About this document</h2>
<p>This document follows the <strong>arc42</strong> template for the publication surface at <code>policy.coulomb.social</code>. It is the first-wave architecture document this repository is allowed to author. Other first-wave systems are written in their owning repos.</p>
</section>
<section id="s1"><h2><span class="sn">01</span>Introduction and Goals</h2>
@ -245,4 +245,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
</section>
<section id="s12"><h2><span class="sn">12</span>Glossary</h2>
<div class="scroll"><table><thead><tr><th>Term</th><th>Meaning</th></tr></thead><tbody><tr><td>Current address</td><td>The stable URL for the document as it now stands.</td></tr><tr><td>Revision address</td><td>Write-once URL for one source digest.</td></tr><tr><td>Publication entry</td><td>One object in <code>publication.json</code>. Discovery is not publication.</td></tr><tr><td>First-wave complete</td><td>Chapters 1, 3, 4, 5.1, 9 and 12 are real; others real or N/A.</td></tr></tbody></table></div>
</section><footer><span>policy-nexus-architecture · draft-1 · proposed</span><span>policy-nexus · docs/architecture/policy-nexus_v0.1.md · b93928330ef7a76976fa62989bb54f47dbe04832</span></footer></main></div></div></html>
</section><footer><span>policy-nexus-architecture · draft-1 · proposed</span><span>policy-nexus · docs/architecture/policy-nexus_v0.1.md · 64b47d73b9fdf33df535db42c80db3a7435da5cf</span></footer></main></div></div></html>

View file

@ -1,6 +1,6 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="db89e5463fe8bb8bf6811cf839548244eab28c4b">
<meta name="policy-source-revision" content="b606d5d44c6bcb3ff438656acae3bf93a786104b">
<meta name="policy-source-digest" content="f65748fa3c861a6f399365ee5315af4e47ec91823837e3d7d89c17d4b385f2aa">
<title>State Hub 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>state-hub-architecture</span> <span class="stat">proposed · draft-1</span> <span>state-hub</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>State Hub architecture</h1><p class="sub">Source: <code>state-hub · docs/architecture/state-hub_v0.1.md · db89e5463fe8bb8bf6811cf839548244eab28c4b</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="#about-this-document"><span class="n">·</span>About this document</a></li><li><a href="#s1"><span class="n">1</span>Introduction and Goals</a></li><li><a href="#s2"><span class="n">2</span>Architecture Constraints</a></li><li><a href="#s3"><span class="n">3</span>System Scope and Context</a></li><li><a href="#s4"><span class="n">4</span>Solution Strategy</a></li><li><a href="#s5"><span class="n">5</span>Building Block View</a></li><li><a href="#s6"><span class="n">6</span>Runtime View</a></li><li><a href="#s7"><span class="n">7</span>Deployment View</a></li><li><a href="#s8"><span class="n">8</span>Cross-Cutting Concepts</a></li><li><a href="#s9"><span class="n">9</span>Architecture Decisions</a></li><li><a href="#s10"><span class="n">10</span>Quality Requirements</a></li><li><a href="#s11"><span class="n">11</span>Risks and Technical Debt</a></li><li><a href="#s12"><span class="n">12</span>Glossary</a></li></ol></nav><main><section id="about-this-document"><h2>About this document</h2>
<div class="wrap"><header><div class="eyebrow"><span>state-hub-architecture</span> <span class="stat">proposed · draft-1</span> <span>state-hub</span> <span>reviewed 2026-08-18</span><span>generated from canonical source — do not edit</span></div><h1>State Hub architecture</h1><p class="sub">Source: <code>state-hub · docs/architecture/state-hub_v0.1.md · b606d5d44c6bcb3ff438656acae3bf93a786104b</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="#about-this-document"><span class="n">·</span>About this document</a></li><li><a href="#s1"><span class="n">1</span>Introduction and Goals</a></li><li><a href="#s2"><span class="n">2</span>Architecture Constraints</a></li><li><a href="#s3"><span class="n">3</span>System Scope and Context</a></li><li><a href="#s4"><span class="n">4</span>Solution Strategy</a></li><li><a href="#s5"><span class="n">5</span>Building Block View</a></li><li><a href="#s6"><span class="n">6</span>Runtime View</a></li><li><a href="#s7"><span class="n">7</span>Deployment View</a></li><li><a href="#s8"><span class="n">8</span>Cross-Cutting Concepts</a></li><li><a href="#s9"><span class="n">9</span>Architecture Decisions</a></li><li><a href="#s10"><span class="n">10</span>Quality Requirements</a></li><li><a href="#s11"><span class="n">11</span>Risks and Technical Debt</a></li><li><a href="#s12"><span class="n">12</span>Glossary</a></li></ol></nav><main><section id="about-this-document"><h2>About this document</h2>
<p>First-wave arc42 for State Hub, the estate's live coordination read-model. This service is in active retirement planning; new permanent ownership should not land here. Chapter 9 points at the estate ADRs that still bind it.</p>
</section>
<section id="s1"><h2><span class="sn">01</span>Introduction and Goals</h2>
@ -243,4 +243,4 @@ a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-off
</section>
<section id="s12"><h2><span class="sn">12</span>Glossary</h2>
<div class="scroll"><table><thead><tr><th>Term</th><th>Meaning</th></tr></thead><tbody><tr><td>Read model</td><td>Derived index; never the origin.</td></tr><tr><td>Registrar</td><td>The single instance allowed to mint workplan UUIDs.</td></tr><tr><td>Retirement</td><td>Coordinated move of capabilities out of this repo.</td></tr></tbody></table></div>
</section><footer><span>state-hub-architecture · draft-1 · proposed</span><span>state-hub · docs/architecture/state-hub_v0.1.md · db89e5463fe8bb8bf6811cf839548244eab28c4b</span></footer></main></div></div></html>
</section><footer><span>state-hub-architecture · draft-1 · proposed</span><span>state-hub · docs/architecture/state-hub_v0.1.md · b606d5d44c6bcb3ff438656acae3bf93a786104b</span></footer></main></div></div></html>

File diff suppressed because one or more lines are too long

View file

@ -13,7 +13,7 @@
"source_digest": "99f802d91a0b3a65f0dac58230d8904f7c61cf3f81eff072fbbc59b634612a8a",
"source_path": "canon/standards/tenancy-posture_v0.1.md",
"source_repo": "net-kingdom",
"source_revision": "ba5d8642e91a31055eafdaef6993d779f6af7e06",
"source_revision": "0e0962e68b39fdcfb633b5058602174a36416a68",
"status": "proposed",
"title": "NetKingdom Tenancy Posture v0.1"
},
@ -64,7 +64,7 @@
"source_digest": "dd42250edcc444fa9a5007c3f9d561984840af2b19a07cf2916cbcc8434df0f7",
"source_path": "docs/architecture/net-kingdom_v0.1.md",
"source_repo": "net-kingdom",
"source_revision": "ba5d8642e91a31055eafdaef6993d779f6af7e06",
"source_revision": "0e0962e68b39fdcfb633b5058602174a36416a68",
"status": "proposed",
"title": "NetKingdom architecture"
},
@ -81,7 +81,7 @@
"source_digest": "f65748fa3c861a6f399365ee5315af4e47ec91823837e3d7d89c17d4b385f2aa",
"source_path": "docs/architecture/state-hub_v0.1.md",
"source_repo": "state-hub",
"source_revision": "db89e5463fe8bb8bf6811cf839548244eab28c4b",
"source_revision": "b606d5d44c6bcb3ff438656acae3bf93a786104b",
"status": "proposed",
"title": "State Hub architecture"
},
@ -98,7 +98,7 @@
"source_digest": "179cbb86bca95f71f46f51ca1971ff1c274eed7d900adf0672b537d4bcc5b480",
"source_path": "docs/architecture/policy-nexus_v0.1.md",
"source_repo": "policy-nexus",
"source_revision": "b93928330ef7a76976fa62989bb54f47dbe04832",
"source_revision": "64b47d73b9fdf33df535db42c80db3a7435da5cf",
"status": "proposed",
"title": "Policy Nexus architecture"
},
@ -115,7 +115,7 @@
"source_digest": "a28668fb4b8b6c5ec8c94baac000061276d85ef1849ec7ab8d132b913dbfe3be",
"source_path": "docs/adr/ADR-0001-addressing-and-permanence.md",
"source_repo": "policy-nexus",
"source_revision": "b93928330ef7a76976fa62989bb54f47dbe04832",
"source_revision": "64b47d73b9fdf33df535db42c80db3a7435da5cf",
"status": "accepted",
"title": "Policy addressing and permanence"
},
@ -254,8 +254,178 @@
"source_revision": "883533ed8af1703cc9bb4a2b24137e325bbbda47",
"status": "accepted",
"title": "Private-by-default Exposure"
},
{
"canonical_path": "adr/activity-core-event-bridge/v1/index.html",
"currency": "current",
"id": "ACT-ADR-001",
"last_reviewed": "2026-05-14",
"lifecycle": "active",
"owner": "activity-core",
"review_due": "2026-11-14",
"revision": "accepted-1",
"revision_path": "adr/activity-core-event-bridge/v1/revisions/accepted-1/index.html",
"source_digest": "ac70015255b8972c7ee38f1a0fb934c6aa5f397634ddc298f0878a8eed6a774a",
"source_path": "docs/adr/adr-001-event-bridge-architecture.md",
"source_repo": "activity-core",
"source_revision": "41a3fb8b81bd521a5fa21af114975c54532df3ad",
"status": "accepted",
"title": "Activity-Core as Coulomb Org Event Bridge"
},
{
"canonical_path": "adr/activity-core-definition-format/v1/index.html",
"currency": "current",
"id": "ACT-ADR-002",
"last_reviewed": "2026-05-14",
"lifecycle": "active",
"owner": "activity-core",
"review_due": "2026-11-14",
"revision": "accepted-1",
"revision_path": "adr/activity-core-definition-format/v1/revisions/accepted-1/index.html",
"source_digest": "157a53907240733148137338c9826a56b77d04d4f41e59c6cb57e6b6f8d9534d",
"source_path": "docs/adr/adr-002-definition-format.md",
"source_repo": "activity-core",
"source_revision": "41a3fb8b81bd521a5fa21af114975c54532df3ad",
"status": "accepted",
"title": "Markdown-as-Definition Format for Event Types and ActivityDefinitions"
},
{
"canonical_path": "adr/activity-core-rule-instruction-model/v1/index.html",
"currency": "current",
"id": "ACT-ADR-003",
"last_reviewed": "2026-05-14",
"lifecycle": "active",
"owner": "activity-core",
"review_due": "2026-11-14",
"revision": "accepted-1",
"revision_path": "adr/activity-core-rule-instruction-model/v1/revisions/accepted-1/index.html",
"source_digest": "81ccfde427525f9a8d47f93c346813bc2d4003a990b503343e27c842a81a2ea6",
"source_path": "docs/adr/adr-003-rule-instruction-model.md",
"source_repo": "activity-core",
"source_revision": "41a3fb8b81bd521a5fa21af114975c54532df3ad",
"status": "accepted",
"title": "Rule vs. Instruction Model and Expression DSL"
},
{
"canonical_path": "adr/activity-core-producer-trust-boundary/v1/index.html",
"currency": "current",
"id": "ACT-ADR-004",
"last_reviewed": "2026-06-26",
"lifecycle": "active",
"owner": "activity-core",
"review_due": "2026-12-26",
"revision": "accepted-1",
"revision_path": "adr/activity-core-producer-trust-boundary/v1/revisions/accepted-1/index.html",
"source_digest": "89b3a925d8cf6d9dbfe426980021b58281350201f316654b7fc1ee6554910ac6",
"source_path": "docs/adr/adr-004-producer-trust-boundary.md",
"source_repo": "activity-core",
"source_revision": "41a3fb8b81bd521a5fa21af114975c54532df3ad",
"status": "accepted",
"title": "The Producer Trust Boundary \u2014 Guardrails and Error-Correction for Untrusted Output"
},
{
"canonical_path": "adr/activity-core-ops-runs-vs-work-records/v1/index.html",
"currency": "current",
"id": "ACT-ADR-005",
"last_reviewed": "2026-08-03",
"lifecycle": "active",
"owner": "activity-core",
"review_due": "2027-02-03",
"revision": "accepted-1",
"revision_path": "adr/activity-core-ops-runs-vs-work-records/v1/revisions/accepted-1/index.html",
"source_digest": "b9005e5f23dce53169e5f614ed461ce49e975266ff9a8fc01e2c6364fc5c91d3",
"source_path": "docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"source_repo": "activity-core",
"source_revision": "41a3fb8b81bd521a5fa21af114975c54532df3ad",
"status": "accepted",
"title": "Ops runs vs development work records \u2014 claim queue and plane split"
},
{
"canonical_path": "adr/ops-warden-catalog-pointer-layer/v1/index.html",
"currency": "current",
"id": "ops-warden-adr-0001",
"last_reviewed": "2026-08-18",
"lifecycle": "active",
"owner": "ops-warden",
"review_due": "2027-02-18",
"revision": "1",
"revision_path": "adr/ops-warden-catalog-pointer-layer/v1/revisions/1/index.html",
"source_digest": "7df0bb364276e382cbee9383e7e67d399b0ac1b0353246e0ab323e629e732a6d",
"source_path": "docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"source_repo": "ops-warden",
"source_revision": "35aff380a33f51a512c1e1b42d52d1dc0d95930f",
"status": "accepted",
"title": "ADR-0001 \u2014 The routing catalog is a pointer layer, never a second copy"
},
{
"canonical_path": "adr/ops-warden-conduit-not-broker/v1/index.html",
"currency": "current",
"id": "ops-warden-adr-0002",
"last_reviewed": "2026-08-18",
"lifecycle": "active",
"owner": "ops-warden",
"review_due": "2027-02-18",
"revision": "1",
"revision_path": "adr/ops-warden-conduit-not-broker/v1/revisions/1/index.html",
"source_digest": "7dcc31732d774ddf2c98636b69ee12e2d74034836ed0def81b7e06461309b53a",
"source_path": "docs/adr/ADR-0002-conduit-not-broker.md",
"source_repo": "ops-warden",
"source_revision": "35aff380a33f51a512c1e1b42d52d1dc0d95930f",
"status": "accepted",
"title": "ADR-0002 \u2014 ops-warden is a transparent conduit, never a secret broker"
},
{
"canonical_path": "adr/ops-warden-cover-gaps/v1/index.html",
"currency": "current",
"id": "ops-warden-adr-0003",
"last_reviewed": "2026-08-18",
"lifecycle": "active",
"owner": "ops-warden",
"review_due": "2027-02-18",
"revision": "1",
"revision_path": "adr/ops-warden-cover-gaps/v1/revisions/1/index.html",
"source_digest": "45b47c02afa575fbfe5be980e426c331a1d99bb9cd9c7a8de80bf8e319469f81",
"source_path": "docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"source_repo": "ops-warden",
"source_revision": "35aff380a33f51a512c1e1b42d52d1dc0d95930f",
"status": "accepted",
"title": "ADR-0003 \u2014 Cover gaps, but never silently own them"
},
{
"canonical_path": "adr/ops-warden-high-risk-read-boundary/v1/index.html",
"currency": "current",
"id": "ops-warden-adr-0004",
"last_reviewed": "2026-08-18",
"lifecycle": "active",
"owner": "ops-warden",
"review_due": "2027-02-18",
"revision": "1",
"revision_path": "adr/ops-warden-high-risk-read-boundary/v1/revisions/1/index.html",
"source_digest": "5db38dcb754af1ba639f4ceca056df842bc7b91fa48dd8bad21611aee29f682d",
"source_path": "docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"source_repo": "ops-warden",
"source_revision": "35aff380a33f51a512c1e1b42d52d1dc0d95930f",
"status": "accepted",
"title": "ADR-0004 \u2014 High-risk lanes refuse raw value streaming to agent sessions"
},
{
"canonical_path": "adr/ops-warden-implement-narrowly/v1/index.html",
"currency": "current",
"id": "ops-warden-adr-0005",
"last_reviewed": "2026-08-18",
"lifecycle": "active",
"owner": "ops-warden",
"review_due": "2027-02-18",
"revision": "1",
"revision_path": "adr/ops-warden-implement-narrowly/v1/revisions/1/index.html",
"source_digest": "31eafe4d8d9362a3446739d63c3af83fd9138cfdc6ad120086312a67dc0d27d0",
"source_path": "docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"source_repo": "ops-warden",
"source_revision": "35aff380a33f51a512c1e1b42d52d1dc0d95930f",
"status": "accepted",
"title": "ADR-0005 \u2014 Implement one lane narrowly, route everything else"
}
],
"generated_as_of": "2026-08-18",
"generated_as_of": "2026-08-19",
"schema_version": 1
}

View file

@ -1,6 +1,6 @@
<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="policy-source-revision" content="ba5d8642e91a31055eafdaef6993d779f6af7e06">
<meta name="policy-source-revision" content="0e0962e68b39fdcfb633b5058602174a36416a68">
<meta name="policy-source-digest" content="99f802d91a0b3a65f0dac58230d8904f7c61cf3f81eff072fbbc59b634612a8a">
<title>NetKingdom Tenancy Posture v0.1</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>netkingdom-tenancy-posture</span> <span class="stat">proposed · draft-8</span> <span>net-kingdom</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom Tenancy Posture v0.1</h1><p class="sub">A framework for describing, holding and improving multi-tenancy — including where we are not there yet.</p><p class="sub">Source: <code>net-kingdom · canon/standards/tenancy-posture_v0.1.md · ba5d8642e91a31055eafdaef6993d779f6af7e06</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="#s0"><span class="n">0</span>Terminology: axes, not planes</a></li><li><a href="#s1"><span class="n">1</span>Context</a></li><li><a href="#s2"><span class="n">2</span>What this document is</a></li><li><a href="#s3"><span class="n">3</span>Six orthogonal axes</a></li><li><a href="#s4"><span class="n">4</span>Graduated levels</a></li><li><a href="#s5"><span class="n">5</span>The posture vector</a></li><li><a href="#s6"><span class="n">6</span>Conformance is accuracy, not altitude</a></li><li><a href="#s7"><span class="n">7</span>Portability across placement levels</a></li><li><a href="#s8"><span class="n">8</span>Placement triggers</a></li><li><a href="#s9"><span class="n">9</span>Credentials as a tenancy control</a></li><li><a href="#s10"><span class="n">10</span>Blast radius must be published</a></li><li><a href="#s11"><span class="n">11</span>Commercial expression</a></li><li><a href="#s12"><span class="n">12</span>Methodology — analyze, establish, improve, guard</a></li><li><a href="#s13"><span class="n">13</span>Evidence per level</a></li><li><a href="#s14"><span class="n">14</span>Adoption stance — structure, not tooling</a></li><li><a href="#s15"><span class="n">15</span>Alternatives considered</a></li><li><a href="#s16"><span class="n">16</span>Held against outside practice</a></li><li><a href="#s17"><span class="n">17</span>Scaling demands</a></li><li><a href="#s18"><span class="n">18</span>Consequences</a></li><li><a href="#s19"><span class="n">19</span>Review resolutions and residual questions</a></li><li><a href="#s20"><span class="n">20</span>Ratification path</a></li></ol></nav><main><section id="status"><h2>Status</h2>
<div class="wrap"><header><div class="eyebrow"><span>netkingdom-tenancy-posture</span> <span class="stat">proposed · draft-8</span> <span>net-kingdom</span> <span>reviewed 2026-08-17</span><span>generated from canonical source — do not edit</span></div><h1>NetKingdom Tenancy Posture v0.1</h1><p class="sub">A framework for describing, holding and improving multi-tenancy — including where we are not there yet.</p><p class="sub">Source: <code>net-kingdom · canon/standards/tenancy-posture_v0.1.md · 0e0962e68b39fdcfb633b5058602174a36416a68</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="#s0"><span class="n">0</span>Terminology: axes, not planes</a></li><li><a href="#s1"><span class="n">1</span>Context</a></li><li><a href="#s2"><span class="n">2</span>What this document is</a></li><li><a href="#s3"><span class="n">3</span>Six orthogonal axes</a></li><li><a href="#s4"><span class="n">4</span>Graduated levels</a></li><li><a href="#s5"><span class="n">5</span>The posture vector</a></li><li><a href="#s6"><span class="n">6</span>Conformance is accuracy, not altitude</a></li><li><a href="#s7"><span class="n">7</span>Portability across placement levels</a></li><li><a href="#s8"><span class="n">8</span>Placement triggers</a></li><li><a href="#s9"><span class="n">9</span>Credentials as a tenancy control</a></li><li><a href="#s10"><span class="n">10</span>Blast radius must be published</a></li><li><a href="#s11"><span class="n">11</span>Commercial expression</a></li><li><a href="#s12"><span class="n">12</span>Methodology — analyze, establish, improve, guard</a></li><li><a href="#s13"><span class="n">13</span>Evidence per level</a></li><li><a href="#s14"><span class="n">14</span>Adoption stance — structure, not tooling</a></li><li><a href="#s15"><span class="n">15</span>Alternatives considered</a></li><li><a href="#s16"><span class="n">16</span>Held against outside practice</a></li><li><a href="#s17"><span class="n">17</span>Scaling demands</a></li><li><a href="#s18"><span class="n">18</span>Consequences</a></li><li><a href="#s19"><span class="n">19</span>Review resolutions and residual questions</a></li><li><a href="#s20"><span class="n">20</span>Ratification path</a></li></ol></nav><main><section id="status"><h2>Status</h2>
<p><strong>Proposed, draft-8; ratification-ready.</strong> Relocated from <code>the-custodian/canon/architecture</code> on 2026-08-17: multi-tenancy is part of the IT-security framework NetKingdom provides, so this framework belongs in NetKingdom canon beside the IAM Profile and the tenant-engine boundary contract, not in the work-factory canon.</p>
<ul><li><strong>draft-1</strong> proposed a single model with fixed characteristics. Rejected: it could not describe a repo that is not there yet.</li><li><strong>draft-2</strong> reframed to graduated levels per axis. Externally corroborated (§16), but four of its statements were wrong and one thing it needed was missing.</li><li><strong>draft-3</strong> applied those corrections, added the retention axis, and recorded an adoption stance.</li><li><strong>draft-4</strong> closed the two gaps draft-3 left open: <code>R4</code> had no mechanism beyond waiting, and the noisy-neighbour evidence artifact asserted something shared infrastructure cannot provide.</li><li><strong>draft-5</strong> relocated to NetKingdom and renamed the dimensions from <em>planes</em> to <em>axes</em>, because the word was already taken (§0).</li><li><strong>draft-6</strong> applied <code>tenant-engine</code>'s review: five changes, including an axis that did not fit its data shape.</li><li><strong>draft-7</strong> applies <code>audit-core</code>, <code>railiance-platform</code> and <code>flex-auth</code>. Eleven further changes, two of them corrections to statements this document made as fact about other repos. <strong>Every posture I guessed was too generous, on every repo that has now self-reported.</strong></li><li><strong>draft-8</strong> applies <code>adaptive-pricing</code>'s review, the last of the six, and the consistency review across all declarations. It adds the missing availability axis, a canonical declaration schema, explicit authority for tier assurance, retention/placement coupling, downgrade propagation, and honest sanctioned customer language. It also corrects the distinction between an implemented control and an evidenced current level.</li></ul>
<p><strong>Reviewed by all six. The score:</strong> six repos found three live defects in their own code by reading the ladders — <code>tenant-engine</code>'s unfiltered event accessor, <code>audit-core</code>'s unfiltered read path, <code>flex-auth</code>'s unauthenticated <code>/v1/check</code> — and <code>railiance-platform</code> found <code>apps-pg</code> running with no backup configured at all while writing its §10.2 disclosure. The framework changed to fit the repos; no repo was told to fabricate a posture.</p>
@ -441,4 +441,4 @@ per consumer: 14 connections (12 runtime + 2 migration)</pre>
</section>
<section id="s20"><h2><span class="sn">20</span>Ratification path</h2>
<ol><li>Reviewed by <code>tenant-engine</code>, <code>flex-auth</code>, <code>audit-core</code>, <code>rapp-postgres</code>, <code>railiance-platform</code> and <code>adaptive-pricing</code> against §19. <strong>Complete in draft-8.</strong></li><li>Each publishes its own posture vector (§5) as part of review. <strong>The framework is validated by whether it can describe them accurately</strong> — if a repo cannot express itself in these six ladders, the ladders are wrong and this document changes, not the repo. <strong>Complete in draft-8; all six root declarations validate against the canonical schema.</strong></li><li>On acceptance, <strong>supersedes</strong> the routing of <code>rapp-postgres/docs/canon-drafts/shared-platform-relational-storage_v0.1-draft.md</code>, whose §§38 are absorbed here. That draft is withdrawn rather than left pending.</li><li>On acceptance, <code>rapp-postgres</code> ADR-0001 through ADR-0004 move to <code>accepted</code> and are annotated as the PostgreSQL implementation of the E, P, R and shared-capacity rules.</li></ol>
</section><footer><span>netkingdom-tenancy-posture · draft-8 · proposed</span><span>net-kingdom · canon/standards/tenancy-posture_v0.1.md · ba5d8642e91a31055eafdaef6993d779f6af7e06</span></footer></main></div></div></html>
</section><footer><span>netkingdom-tenancy-posture · draft-8 · proposed</span><span>net-kingdom · canon/standards/tenancy-posture_v0.1.md · 0e0962e68b39fdcfb633b5058602174a36416a68</span></footer></main></div></div></html>

View file

@ -1,20 +1,20 @@
# ADR review ledger summary
Rows: 130
Rows: 136
## Inventory dispositions
- `excluded`: 2
- `metadata-pending`: 105
- `published`: 15
- `excluded`: 3
- `metadata-pending`: 100
- `published`: 25
- `unsupported-format`: 8
## Proposed dispositions
- `conflict`: 5
- `publish`: 27
- `publish`: 32
- `superseded`: 5
- `unreviewed`: 93
- `unreviewed`: 94
## Front-matter `id` collisions
@ -28,11 +28,11 @@ Rows: 130
## Bare ADR-NNNN collisions
- `ADR-0001`: activity-core/docs/adr/adr-001-event-bridge-architecture.md, coulomb-loop/docs/adr/ADR-001-workplan-prefix.md, coulomb-social/docs/adr/ADR-0001-netkingdom-identity.md, evidence-binder/docs/adr/ADR-0001-reference-ui-surface.md, glas-harness/docs/adr/ADR-001-rein-harness-family.md, kaizen-agentic/docs/adr/ADR-001-workplan-convention.md, key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md, markitect-main/docs/adr/ADR-001-client-side-debug-storage.md, policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md, railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md, railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md, rapp-postgres/docs/adr/ADR-0001-consumer-boundary-and-tenant-isolation.md, rein-aharness/docs/adr/ADR-001-agent-harness-architecture.md, target-revenue/docs/adr/ADR-0001-stage0-library-stack.md, the-custodian/canon/architecture/adr-001-workplans-as-repo-artefacts.md
- `ADR-0002`: activity-core/docs/adr/adr-002-definition-format.md, coulomb-loop/docs/adr/ADR-002-customer-supplier-boundary.md, coulomb-social/docs/adr/ADR-0002-space-content-forgejo-markdown.md, glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md, kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md, markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md, railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md, railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md, railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md, railiance-platform/docs/adr/ADR-0002-placement-policy-ownership.md, rapp-postgres/docs/adr/ADR-0002-data-retention-and-erasure.md, target-revenue/docs/adr/ADR-0002-hosted-trust-service-stack.md, the-custodian/canon/architecture/adr-002-custodian-agent-runtime-design.md
- `ADR-0003`: activity-core/docs/adr/adr-003-rule-instruction-model.md, coulomb-loop/docs/adr/ADR-003-cadence-ramp-policy.md, coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md, glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md, kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md, railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md, railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md, railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md, railiance-platform/docs/adr/ADR-0003-decisions-live-in-the-repo.md, rapp-postgres/docs/adr/ADR-0003-e3-row-level-security-contract.md, the-custodian/canon/architecture/adr-003-materialized-derived-state.md
- `ADR-0004`: activity-core/docs/adr/adr-004-producer-trust-boundary.md, coulomb-loop/docs/adr/ADR-004-repo-rotation-on-diminishing-returns.md, coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md, glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md, kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md, railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md, railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md, railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md, rapp-postgres/docs/adr/ADR-0004-platform-pg-cell-ceiling.md, the-custodian/canon/architecture/adr-004-connectivity-first-network-posture.md
- `ADR-0005`: activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md, kaizen-agentic/docs/adr/ADR-005-scheduled-agent-execution.md, railiance-infra/docs/adr/ADR-005-k3s-api-tunnel-only.md, railiance-master/docs/adr/ADR-0005-derived-rail-composition.md, the-custodian/canon/architecture/adr-005-cross-repo-workplans-project-repos.md
- `ADR-0001`: activity-core/docs/adr/adr-001-event-bridge-architecture.md, coulomb-loop/docs/adr/ADR-001-workplan-prefix.md, coulomb-social/docs/adr/ADR-0001-netkingdom-identity.md, evidence-binder/docs/adr/ADR-0001-reference-ui-surface.md, glas-harness/docs/adr/ADR-001-rein-harness-family.md, kaizen-agentic/docs/adr/ADR-001-workplan-convention.md, key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md, markitect-main/docs/adr/ADR-001-client-side-debug-storage.md, ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md, policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md, railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md, railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md, rapp-postgres/docs/adr/ADR-0001-consumer-boundary-and-tenant-isolation.md, rein-aharness/docs/adr/ADR-001-agent-harness-architecture.md, target-revenue/docs/adr/ADR-0001-stage0-library-stack.md, the-custodian/canon/architecture/adr-001-workplans-as-repo-artefacts.md
- `ADR-0002`: activity-core/docs/adr/adr-002-definition-format.md, coulomb-loop/docs/adr/ADR-002-customer-supplier-boundary.md, coulomb-social/docs/adr/ADR-0002-space-content-forgejo-markdown.md, glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md, kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md, markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md, ops-warden/docs/adr/ADR-0002-conduit-not-broker.md, railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md, railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md, railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md, railiance-platform/docs/adr/ADR-0002-placement-policy-ownership.md, rapp-postgres/docs/adr/ADR-0002-data-retention-and-erasure.md, target-revenue/docs/adr/ADR-0002-hosted-trust-service-stack.md, the-custodian/canon/architecture/adr-002-custodian-agent-runtime-design.md
- `ADR-0003`: activity-core/docs/adr/adr-003-rule-instruction-model.md, coulomb-loop/docs/adr/ADR-003-cadence-ramp-policy.md, coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md, glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md, kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md, ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md, railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md, railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md, railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md, railiance-platform/docs/adr/ADR-0003-decisions-live-in-the-repo.md, rapp-postgres/docs/adr/ADR-0003-e3-row-level-security-contract.md, the-custodian/canon/architecture/adr-003-materialized-derived-state.md
- `ADR-0004`: activity-core/docs/adr/adr-004-producer-trust-boundary.md, coulomb-loop/docs/adr/ADR-004-repo-rotation-on-diminishing-returns.md, coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md, glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md, kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md, ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md, railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md, railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md, railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md, rapp-postgres/docs/adr/ADR-0004-platform-pg-cell-ceiling.md, the-custodian/canon/architecture/adr-004-connectivity-first-network-posture.md
- `ADR-0005`: activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md, kaizen-agentic/docs/adr/ADR-005-scheduled-agent-execution.md, ops-warden/docs/adr/ADR-0005-implement-narrowly-route-broadly.md, railiance-infra/docs/adr/ADR-005-k3s-api-tunnel-only.md, railiance-master/docs/adr/ADR-0005-derived-rail-composition.md, the-custodian/canon/architecture/adr-005-cross-repo-workplans-project-repos.md
- `ADR-0006`: kaizen-agentic/docs/adr/ADR-006-customer-engagement-convention.md, net-kingdom/docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md, railiance-master/docs/adr/ADR-0006-reef-production-admission.md, the-custodian/canon/architecture/adr-006-canon-federation-concept-ownership.md
- `ADR-0007`: kaizen-agentic/docs/adr/ADR-007-forward-deployed-engagement-convention.md, net-kingdom/docs/adr/ADR-0007-security-orchestration-boundary.md, railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md, the-custodian/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md
- `ADR-0008`: net-kingdom/docs/adr/ADR-0008-object-storage-sts-credential-vending.md, railiance-master/docs/adr/ADR-0008-private-by-default-exposure.md, the-custodian/canon/architecture/adr-008-multi-tenancy-model.md

View file

@ -11,6 +11,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -23,23 +24,19 @@
"file_present": true,
"frontmatter": {
"id": "ACT-ADR-001",
"last_reviewed": "",
"owner": "",
"review_interval": "",
"revision": "",
"last_reviewed": "2026-05-14",
"owner": "activity-core",
"review_interval": "6m",
"revision": "accepted-1",
"status": "accepted",
"title": "Activity-Core as Coulomb Org Event Bridge",
"updated": "",
"version": ""
},
"id_collisions": [],
"inventory_disposition": "metadata-pending",
"inventory_reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"missing_fields": [
"owner",
"revision|version",
"review"
],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Cross-repo event bridge. Id ACT-ADR-001 is unique. Needs owner/revision/review front-matter.",
@ -55,6 +52,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -67,23 +65,19 @@
"file_present": true,
"frontmatter": {
"id": "ACT-ADR-002",
"last_reviewed": "",
"owner": "",
"review_interval": "",
"revision": "",
"last_reviewed": "2026-05-14",
"owner": "activity-core",
"review_interval": "6m",
"revision": "accepted-1",
"status": "accepted",
"title": "Markdown-as-Definition Format for Event Types and ActivityDefinitions",
"updated": "",
"version": ""
},
"id_collisions": [],
"inventory_disposition": "metadata-pending",
"inventory_reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"missing_fields": [
"owner",
"revision|version",
"review"
],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Cross-repo definition format. Unique id. Needs owner/revision/review.",
@ -98,6 +92,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -109,23 +104,19 @@
"file_present": true,
"frontmatter": {
"id": "ACT-ADR-003",
"last_reviewed": "",
"owner": "",
"review_interval": "",
"revision": "",
"last_reviewed": "2026-05-14",
"owner": "activity-core",
"review_interval": "6m",
"revision": "accepted-1",
"status": "accepted",
"title": "Rule vs. Instruction Model and Expression DSL",
"updated": "",
"version": ""
},
"id_collisions": [],
"inventory_disposition": "metadata-pending",
"inventory_reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"missing_fields": [
"owner",
"revision|version",
"review"
],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Cross-repo rule/instruction split. Unique id. Needs owner/revision/review.",
@ -140,6 +131,7 @@
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
@ -150,23 +142,19 @@
"file_present": true,
"frontmatter": {
"id": "ACT-ADR-004",
"last_reviewed": "",
"owner": "",
"review_interval": "",
"revision": "",
"last_reviewed": "2026-06-26",
"owner": "activity-core",
"review_interval": "6m",
"revision": "accepted-1",
"status": "accepted",
"title": "The Producer Trust Boundary \u2014 Guardrails and Error-Correction for Untrusted Output",
"updated": "",
"version": ""
},
"id_collisions": [],
"inventory_disposition": "metadata-pending",
"inventory_reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"missing_fields": [
"owner",
"revision|version",
"review"
],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Cross-repo producer trust boundary. Unique id. Needs owner/revision/review.",
@ -178,6 +166,7 @@
"bare_adr": "ADR-0005",
"bare_adr_collisions": [
"kaizen-agentic/docs/adr/ADR-005-scheduled-agent-execution.md",
"ops-warden/docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"railiance-infra/docs/adr/ADR-005-k3s-api-tunnel-only.md",
"railiance-master/docs/adr/ADR-0005-derived-rail-composition.md",
"the-custodian/canon/architecture/adr-005-cross-repo-workplans-project-repos.md"
@ -186,23 +175,19 @@
"file_present": true,
"frontmatter": {
"id": "ACT-ADR-005",
"last_reviewed": "",
"owner": "",
"review_interval": "",
"revision": "",
"last_reviewed": "2026-08-03",
"owner": "activity-core",
"review_interval": "6m",
"revision": "accepted-1",
"status": "accepted",
"title": "Ops runs vs development work records \u2014 claim queue and plane split",
"updated": "",
"version": ""
},
"id_collisions": [],
"inventory_disposition": "metadata-pending",
"inventory_reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"missing_fields": [
"owner",
"revision|version",
"review"
],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Cross-repo ops-run vs work-record split. Unique id. Needs owner/revision/review.",
@ -492,6 +477,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -542,6 +528,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -590,6 +577,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -637,6 +625,7 @@
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
@ -686,6 +675,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -735,6 +725,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -783,6 +774,7 @@
"coulomb-loop/docs/adr/ADR-003-cadence-ramp-policy.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -825,6 +817,7 @@
"coulomb-loop/docs/adr/ADR-004-repo-rotation-on-diminishing-returns.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
@ -869,6 +862,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -1054,6 +1048,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -1101,6 +1096,7 @@
"coulomb-social/docs/adr/ADR-0002-space-content-forgejo-markdown.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -1147,6 +1143,7 @@
"coulomb-loop/docs/adr/ADR-003-cadence-ramp-policy.md",
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -1192,6 +1189,7 @@
"coulomb-loop/docs/adr/ADR-004-repo-rotation-on-diminishing-returns.md",
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
@ -1239,6 +1237,7 @@
"glas-harness/docs/adr/ADR-001-rein-harness-family.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -1289,6 +1288,7 @@
"coulomb-social/docs/adr/ADR-0002-space-content-forgejo-markdown.md",
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -1337,6 +1337,7 @@
"coulomb-loop/docs/adr/ADR-003-cadence-ramp-policy.md",
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -1384,6 +1385,7 @@
"coulomb-loop/docs/adr/ADR-004-repo-rotation-on-diminishing-returns.md",
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
@ -1427,6 +1429,7 @@
"bare_adr": "ADR-0005",
"bare_adr_collisions": [
"activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"ops-warden/docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"railiance-infra/docs/adr/ADR-005-k3s-api-tunnel-only.md",
"railiance-master/docs/adr/ADR-0005-derived-rail-composition.md",
"the-custodian/canon/architecture/adr-005-cross-repo-workplans-project-repos.md"
@ -1549,6 +1552,7 @@
"glas-harness/docs/adr/ADR-001-rein-harness-family.md",
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -1600,6 +1604,7 @@
"glas-harness/docs/adr/ADR-001-rein-harness-family.md",
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -1647,6 +1652,7 @@
"coulomb-social/docs/adr/ADR-0002-space-content-forgejo-markdown.md",
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -2267,6 +2273,235 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
"rapp-postgres/docs/adr/ADR-0001-consumer-boundary-and-tenant-isolation.md",
"rein-aharness/docs/adr/ADR-001-agent-harness-architecture.md",
"target-revenue/docs/adr/ADR-0001-stage0-library-stack.md",
"the-custodian/canon/architecture/adr-001-workplans-as-repo-artefacts.md"
],
"conflict_kinds": [],
"file_present": true,
"frontmatter": {
"id": "ops-warden-adr-0001",
"last_reviewed": "2026-08-18",
"owner": "ops-warden",
"review_interval": "6m",
"revision": "1",
"status": "accepted",
"title": "ADR-0001 \u2014 The routing catalog is a pointer layer, never a second copy",
"updated": "2026-08-18",
"version": "1.0"
},
"id_collisions": [],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Binds catalog contributors. Publication-ready.",
"source_path": "docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"source_repo": "ops-warden",
"successor": ""
},
{
"bare_adr": "ADR-0002",
"bare_adr_collisions": [
"activity-core/docs/adr/adr-002-definition-format.md",
"coulomb-loop/docs/adr/ADR-002-customer-supplier-boundary.md",
"coulomb-social/docs/adr/ADR-0002-space-content-forgejo-markdown.md",
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
"railiance-platform/docs/adr/ADR-0002-placement-policy-ownership.md",
"rapp-postgres/docs/adr/ADR-0002-data-retention-and-erasure.md",
"target-revenue/docs/adr/ADR-0002-hosted-trust-service-stack.md",
"the-custodian/canon/architecture/adr-002-custodian-agent-runtime-design.md"
],
"conflict_kinds": [],
"file_present": true,
"frontmatter": {
"id": "ops-warden-adr-0002",
"last_reviewed": "2026-08-18",
"owner": "ops-warden",
"review_interval": "6m",
"revision": "1",
"status": "accepted",
"title": "ADR-0002 \u2014 ops-warden is a transparent conduit, never a secret broker",
"updated": "2026-08-18",
"version": "1.0"
},
"id_collisions": [],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Owner-ready; harvest request 2026-08-18.",
"source_path": "docs/adr/ADR-0002-conduit-not-broker.md",
"source_repo": "ops-warden",
"successor": ""
},
{
"bare_adr": "ADR-0003",
"bare_adr_collisions": [
"activity-core/docs/adr/adr-003-rule-instruction-model.md",
"coulomb-loop/docs/adr/ADR-003-cadence-ramp-policy.md",
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
"railiance-platform/docs/adr/ADR-0003-decisions-live-in-the-repo.md",
"rapp-postgres/docs/adr/ADR-0003-e3-row-level-security-contract.md",
"the-custodian/canon/architecture/adr-003-materialized-derived-state.md"
],
"conflict_kinds": [],
"file_present": true,
"frontmatter": {
"id": "ops-warden-adr-0003",
"last_reviewed": "2026-08-18",
"owner": "ops-warden",
"review_interval": "6m",
"revision": "1",
"status": "accepted",
"title": "ADR-0003 \u2014 Cover gaps, but never silently own them",
"updated": "2026-08-18",
"version": "1.0"
},
"id_collisions": [],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Owner-ready; harvest request 2026-08-18.",
"source_path": "docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"source_repo": "ops-warden",
"successor": ""
},
{
"bare_adr": "ADR-0004",
"bare_adr_collisions": [
"activity-core/docs/adr/adr-004-producer-trust-boundary.md",
"coulomb-loop/docs/adr/ADR-004-repo-rotation-on-diminishing-returns.md",
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
"rapp-postgres/docs/adr/ADR-0004-platform-pg-cell-ceiling.md",
"the-custodian/canon/architecture/adr-004-connectivity-first-network-posture.md"
],
"conflict_kinds": [],
"file_present": true,
"frontmatter": {
"id": "ops-warden-adr-0004",
"last_reviewed": "2026-08-18",
"owner": "ops-warden",
"review_interval": "6m",
"revision": "1",
"status": "accepted",
"title": "ADR-0004 \u2014 High-risk lanes refuse raw value streaming to agent sessions",
"updated": "2026-08-18",
"version": "1.0"
},
"id_collisions": [],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Binds agent runtimes calling warden access.",
"source_path": "docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"source_repo": "ops-warden",
"successor": ""
},
{
"bare_adr": "ADR-0005",
"bare_adr_collisions": [
"activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"kaizen-agentic/docs/adr/ADR-005-scheduled-agent-execution.md",
"railiance-infra/docs/adr/ADR-005-k3s-api-tunnel-only.md",
"railiance-master/docs/adr/ADR-0005-derived-rail-composition.md",
"the-custodian/canon/architecture/adr-005-cross-repo-workplans-project-repos.md"
],
"conflict_kinds": [],
"file_present": true,
"frontmatter": {
"id": "ops-warden-adr-0005",
"last_reviewed": "2026-08-18",
"owner": "ops-warden",
"review_interval": "6m",
"revision": "1",
"status": "accepted",
"title": "ADR-0005 \u2014 Implement one lane narrowly, route everything else",
"updated": "2026-08-18",
"version": "1.0"
},
"id_collisions": [],
"inventory_disposition": "published",
"inventory_reason": "Published through an explicit publication.json document entry.",
"missing_fields": [],
"notes": "",
"proposed_disposition": "publish",
"review_notes": "Owner-ready; harvest request 2026-08-18.",
"source_path": "docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"source_repo": "ops-warden",
"successor": ""
},
{
"bare_adr": "",
"bare_adr_collisions": [],
"conflict_kinds": [],
"file_present": true,
"frontmatter": {
"id": "",
"last_reviewed": "",
"owner": "",
"review_interval": "",
"revision": "",
"status": "",
"title": "",
"updated": "",
"version": ""
},
"id_collisions": [],
"inventory_disposition": "excluded",
"inventory_reason": "Directory index, not an architecture decision record.",
"missing_fields": [
"id",
"title",
"status",
"owner",
"revision|version",
"review"
],
"notes": "",
"proposed_disposition": "unreviewed",
"review_notes": "",
"source_path": "docs/adr/README.md",
"source_repo": "ops-warden",
"successor": ""
},
{
"bare_adr": "ADR-0001",
"bare_adr_collisions": [
"activity-core/docs/adr/adr-001-event-bridge-architecture.md",
"coulomb-loop/docs/adr/ADR-001-workplan-prefix.md",
"coulomb-social/docs/adr/ADR-0001-netkingdom-identity.md",
"evidence-binder/docs/adr/ADR-0001-reference-ui-surface.md",
"glas-harness/docs/adr/ADR-001-rein-harness-family.md",
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
"rapp-postgres/docs/adr/ADR-0001-consumer-boundary-and-tenant-isolation.md",
@ -2334,6 +2569,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
"railiance-platform/docs/adr/ADR-0002-placement-policy-ownership.md",
@ -2383,6 +2619,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
"railiance-platform/docs/adr/ADR-0003-decisions-live-in-the-repo.md",
@ -2430,6 +2667,7 @@
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
"rapp-postgres/docs/adr/ADR-0004-platform-pg-cell-ceiling.md",
@ -2477,6 +2715,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
"railiance-platform/docs/adr/ADR-0002-placement-policy-ownership.md",
@ -2526,6 +2765,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
"railiance-platform/docs/adr/ADR-0003-decisions-live-in-the-repo.md",
@ -2573,6 +2813,7 @@
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
"rapp-postgres/docs/adr/ADR-0004-platform-pg-cell-ceiling.md",
@ -2616,6 +2857,7 @@
"bare_adr_collisions": [
"activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"kaizen-agentic/docs/adr/ADR-005-scheduled-agent-execution.md",
"ops-warden/docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"railiance-master/docs/adr/ADR-0005-derived-rail-composition.md",
"the-custodian/canon/architecture/adr-005-cross-repo-workplans-project-repos.md"
],
@ -2661,6 +2903,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
"rapp-postgres/docs/adr/ADR-0001-consumer-boundary-and-tenant-isolation.md",
@ -2701,6 +2944,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-platform/docs/adr/ADR-0002-placement-policy-ownership.md",
@ -2740,6 +2984,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-platform/docs/adr/ADR-0003-decisions-live-in-the-repo.md",
@ -2778,6 +3023,7 @@
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"rapp-postgres/docs/adr/ADR-0004-platform-pg-cell-ceiling.md",
@ -2812,6 +3058,7 @@
"bare_adr_collisions": [
"activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"kaizen-agentic/docs/adr/ADR-005-scheduled-agent-execution.md",
"ops-warden/docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"railiance-infra/docs/adr/ADR-005-k3s-api-tunnel-only.md",
"the-custodian/canon/architecture/adr-005-cross-repo-workplans-project-repos.md"
],
@ -2969,6 +3216,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"rapp-postgres/docs/adr/ADR-0001-consumer-boundary-and-tenant-isolation.md",
@ -3011,6 +3259,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -3052,6 +3301,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -3124,6 +3374,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -3171,6 +3422,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -3217,6 +3469,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -3262,6 +3515,7 @@
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
@ -3309,6 +3563,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -3388,6 +3643,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -3437,6 +3693,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -3483,6 +3740,7 @@
"kaizen-agentic/docs/adr/ADR-001-workplan-convention.md",
"key-cape/docs/adr/ADR-0001-choose-go-for-keycape.md",
"markitect-main/docs/adr/ADR-001-client-side-debug-storage.md",
"ops-warden/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"policy-nexus/docs/adr/ADR-0001-addressing-and-permanence.md",
"railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md",
"railiance-platform/docs/adr/ADR-0001-s3-platform-service-boundary.md",
@ -3533,6 +3791,7 @@
"glas-harness/docs/adr/ADR-002-credential-brokering-and-composable-reins.md",
"kaizen-agentic/docs/adr/ADR-002-project-memory-convention.md",
"markitect-main/docs/adr/ADR-002-robustness-principle-for-production-use.md",
"ops-warden/docs/adr/ADR-0002-conduit-not-broker.md",
"railiance-hosts/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-infra/docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"railiance-master/docs/adr/ADR-0002-rail-kubernetes-wave-1-boundary.md",
@ -3581,6 +3840,7 @@
"coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"glas-harness/docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md",
"kaizen-agentic/docs/adr/ADR-003-protocols-artifact-convention.md",
"ops-warden/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md",
@ -3628,6 +3888,7 @@
"coulomb-social/docs/adr/ADR-0004-content-plane-thin-git-upgrades.md",
"glas-harness/docs/adr/ADR-004-composable-reins-stay-deferred.md",
"kaizen-agentic/docs/adr/ADR-004-project-metrics-convention.md",
"ops-warden/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"railiance-hosts/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-infra/docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"railiance-master/docs/adr/ADR-0004-first-wave-reef-rollout.md",
@ -3671,6 +3932,7 @@
"bare_adr_collisions": [
"activity-core/docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"kaizen-agentic/docs/adr/ADR-005-scheduled-agent-execution.md",
"ops-warden/docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"railiance-infra/docs/adr/ADR-005-k3s-api-tunnel-only.md",
"railiance-master/docs/adr/ADR-0005-derived-rail-composition.md"
],

View file

@ -8,7 +8,9 @@
"source_repo": "the-custodian",
"source_path": "canon/architecture/adr-008-multi-tenancy-model.md",
"proposed_disposition": "superseded",
"conflict_kinds": [6],
"conflict_kinds": [
6
],
"successor": "netkingdom-tenancy-posture",
"review_notes": "Body and front-matter already say relocated to Tenancy Posture. Not a live duplicate claim. Who ruled: T04 from the document itself."
},
@ -16,7 +18,9 @@
"source_repo": "the-custodian",
"source_path": "canon/standards/iam-profile_v0.1.md",
"proposed_disposition": "superseded",
"conflict_kinds": [3],
"conflict_kinds": [
3
],
"successor": "net-kingdom/canon/standards/iam-profile_v0.3.md",
"review_notes": "Already superseded. Recorded successor still points at v0.2; v0.3 now supersedes v0.2. Who rules: the-custodian to update superseded_by."
},
@ -24,7 +28,11 @@
"source_repo": "net-kingdom",
"source_path": "canon/standards/iam-profile_v0.2.md",
"proposed_disposition": "superseded",
"conflict_kinds": [1, 2, 6],
"conflict_kinds": [
1,
2,
6
],
"successor": "net-kingdom/canon/standards/iam-profile_v0.3.md",
"review_notes": "v0.3 says it supersedes v0.2, but v0.2 is still status accepted and shares id netkingdom-iam-profile. Who rules: net-kingdom (status + unique id)."
},
@ -32,7 +40,10 @@
"source_repo": "net-kingdom",
"source_path": "canon/standards/iam-profile_v0.3.md",
"proposed_disposition": "publish",
"conflict_kinds": [1, 6],
"conflict_kinds": [
1,
6
],
"successor": "",
"review_notes": "Current IAM profile. Cannot publish until id is unique (e.g. netkingdom-iam-profile-v0.3). Who rules: net-kingdom."
},
@ -40,7 +51,10 @@
"source_repo": "railiance-hosts",
"source_path": "docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"proposed_disposition": "superseded",
"conflict_kinds": [2, 6],
"conflict_kinds": [
2,
6
],
"successor": "railiance-hosts/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"review_notes": "Body is superseded by ADR-003. Byte-identical copy also lives in railiance-infra. Who rules: railiance-hosts/infra which copy is historical."
},
@ -48,7 +62,10 @@
"source_repo": "railiance-infra",
"source_path": "docs/adr/ADR-002-repo-boundary-hosts-vs-bootstrap.md",
"proposed_disposition": "superseded",
"conflict_kinds": [2, 6],
"conflict_kinds": [
2,
6
],
"successor": "railiance-infra/docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"review_notes": "Identical to the railiance-hosts copy; both say superseded by ADR-003."
},
@ -56,7 +73,9 @@
"source_repo": "railiance-hosts",
"source_path": "docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"proposed_disposition": "conflict",
"conflict_kinds": [6],
"conflict_kinds": [
6
],
"successor": "",
"review_notes": "Accepted 5-repo stack ADR is byte-identical in railiance-hosts and railiance-infra. Two current copies of one decision. Who rules: those two repos, or railiance-master if the stack story moved."
},
@ -64,7 +83,9 @@
"source_repo": "railiance-infra",
"source_path": "docs/adr/ADR-003-railiance-5repo-stack-architecture.md",
"proposed_disposition": "conflict",
"conflict_kinds": [6],
"conflict_kinds": [
6
],
"successor": "",
"review_notes": "Identical accepted copy of the hosts ADR-003. Do not publish both."
},
@ -72,7 +93,9 @@
"source_repo": "railiance-hosts",
"source_path": "docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"proposed_disposition": "conflict",
"conflict_kinds": [6],
"conflict_kinds": [
6
],
"successor": "",
"review_notes": "Accepted Forgejo runner ADR is byte-identical in railiance-hosts and railiance-infra. Who rules: those two repos."
},
@ -80,7 +103,9 @@
"source_repo": "railiance-infra",
"source_path": "docs/adr/ADR-004-forgejo-in-cluster-actions-runner.md",
"proposed_disposition": "conflict",
"conflict_kinds": [6],
"conflict_kinds": [
6
],
"successor": "",
"review_notes": "Identical accepted copy of the hosts ADR-004. Do not publish both."
},
@ -88,7 +113,9 @@
"source_repo": "coulomb-social",
"source_path": "docs/adr/ADR-0002-space-content-forgejo-markdown.md",
"proposed_disposition": "conflict",
"conflict_kinds": [2],
"conflict_kinds": [
2
],
"successor": "coulomb-social/docs/adr/ADR-0003-page-centric-markdown-sor.md",
"review_notes": "Status is 'superseded in part' by ADR-0003/0004. Not a clean superseded or accepted. Who rules: coulomb-social."
},
@ -96,7 +123,9 @@
"source_repo": "the-custodian",
"source_path": "canon/architecture/adr-001-workplans-as-repo-artefacts.md",
"proposed_disposition": "publish",
"conflict_kinds": [1],
"conflict_kinds": [
1
],
"successor": "",
"review_notes": "Estate rule: workplans originate as repo files. Needs a unique publication id (e.g. CUST-ADR-001) before publish."
},
@ -104,7 +133,9 @@
"source_repo": "the-custodian",
"source_path": "canon/architecture/adr-002-custodian-agent-runtime-design.md",
"proposed_disposition": "publish",
"conflict_kinds": [1],
"conflict_kinds": [
1
],
"successor": "",
"review_notes": "Estate ADR. Prefix the publication id before publish."
},
@ -112,7 +143,9 @@
"source_repo": "the-custodian",
"source_path": "canon/architecture/adr-003-materialized-derived-state.md",
"proposed_disposition": "publish",
"conflict_kinds": [1],
"conflict_kinds": [
1
],
"successor": "",
"review_notes": "Estate ADR: hub is a derived read-model. Prefix the publication id before publish."
},
@ -120,7 +153,9 @@
"source_repo": "the-custodian",
"source_path": "canon/architecture/adr-004-connectivity-first-network-posture.md",
"proposed_disposition": "publish",
"conflict_kinds": [1],
"conflict_kinds": [
1
],
"successor": "",
"review_notes": "Estate ADR. Prefix the publication id before publish."
},
@ -128,7 +163,9 @@
"source_repo": "the-custodian",
"source_path": "canon/architecture/adr-005-cross-repo-workplans-project-repos.md",
"proposed_disposition": "publish",
"conflict_kinds": [1],
"conflict_kinds": [
1
],
"successor": "",
"review_notes": "Estate ADR. Prefix the publication id before publish."
},
@ -227,6 +264,46 @@
"conflict_kinds": [],
"successor": "",
"review_notes": "Restates ADR-001 for this repo. Needs a unique publication id (front-matter has title but no id)."
},
{
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"proposed_disposition": "publish",
"conflict_kinds": [],
"successor": "",
"review_notes": "Binds catalog contributors. Publication-ready."
},
{
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0002-conduit-not-broker.md",
"proposed_disposition": "publish",
"conflict_kinds": [],
"successor": "",
"review_notes": "Owner-ready; harvest request 2026-08-18."
},
{
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"proposed_disposition": "publish",
"conflict_kinds": [],
"successor": "",
"review_notes": "Owner-ready; harvest request 2026-08-18."
},
{
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"proposed_disposition": "publish",
"conflict_kinds": [],
"successor": "",
"review_notes": "Binds agent runtimes calling warden access."
},
{
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"proposed_disposition": "publish",
"conflict_kinds": [],
"successor": "",
"review_notes": "Owner-ready; harvest request 2026-08-18."
}
]
}

View file

@ -91,15 +91,14 @@ One markdown file per system, following
- A stub that can be published early has real chapters **1, 3 and 9**
plus this front-matter.
Validate once Markitect registers `arc42-v1`:
Validate:
```sh
markitect validate docs/architecture/<system>_v0.1.md --schema arc42-v1
markitect validate docs/architecture/<system>_v0.1.md \
--schema /path/to/markitect-main/markitect/schemas/arc42-schema-v1.0.md
```
Until that schema exists, author against the template. This repo will
not publish an architecture document that cannot be validated after the
schema lands.
Catalog id: `arc42-v1`.
## Infospace index
@ -108,6 +107,11 @@ The collection index lives at
owning-repo documents. It does not copy them. InfoTechCanon owns the
information-space model, not this estate's building-block view.
```sh
markitect infospace check --config \
the-custodian/canon/architecture/infospace/infospace.yaml
```
## How to request publication
1. Put the front-matter on the source document in the owning repo.

View file

@ -13,11 +13,27 @@
"projects": false
},
"repositories": {
"net-kingdom": {"path": "../net-kingdom"},
"policy-nexus": {"path": "."},
"railiance-master": {"path": "../railiance-master"},
"state-hub": {"path": "../state-hub"},
"the-custodian": {"path": "../the-custodian"}
"net-kingdom": {
"path": "../net-kingdom"
},
"policy-nexus": {
"path": "."
},
"railiance-master": {
"path": "../railiance-master"
},
"state-hub": {
"path": "../state-hub"
},
"the-custodian": {
"path": "../the-custodian"
},
"activity-core": {
"path": "../activity-core"
},
"ops-warden": {
"path": "../ops-warden"
}
},
"documents": [
{
@ -26,8 +42,10 @@
"source_path": "canon/standards/tenancy-posture_v0.1.md",
"canonical_path": "standards/tenancy-posture/v0.1/index.html",
"revision_path": "standards/tenancy-posture/v0.1/revisions/{revision}/index.html",
"legacy_paths": ["tenancy-posture.html"],
"subtitle": "A framework for describing, holding and improving multi-tenancy — including where we are not there yet.",
"legacy_paths": [
"tenancy-posture.html"
],
"subtitle": "A framework for describing, holding and improving multi-tenancy \u2014 including where we are not there yet.",
"review_interval": "6m"
},
{
@ -141,6 +159,86 @@
"canonical_path": "adr/railiance-private-by-default-exposure/v1/index.html",
"revision_path": "adr/railiance-private-by-default-exposure/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ACT-ADR-001",
"source_repo": "activity-core",
"source_path": "docs/adr/adr-001-event-bridge-architecture.md",
"canonical_path": "adr/activity-core-event-bridge/v1/index.html",
"revision_path": "adr/activity-core-event-bridge/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ACT-ADR-002",
"source_repo": "activity-core",
"source_path": "docs/adr/adr-002-definition-format.md",
"canonical_path": "adr/activity-core-definition-format/v1/index.html",
"revision_path": "adr/activity-core-definition-format/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ACT-ADR-003",
"source_repo": "activity-core",
"source_path": "docs/adr/adr-003-rule-instruction-model.md",
"canonical_path": "adr/activity-core-rule-instruction-model/v1/index.html",
"revision_path": "adr/activity-core-rule-instruction-model/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ACT-ADR-004",
"source_repo": "activity-core",
"source_path": "docs/adr/adr-004-producer-trust-boundary.md",
"canonical_path": "adr/activity-core-producer-trust-boundary/v1/index.html",
"revision_path": "adr/activity-core-producer-trust-boundary/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ACT-ADR-005",
"source_repo": "activity-core",
"source_path": "docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"canonical_path": "adr/activity-core-ops-runs-vs-work-records/v1/index.html",
"revision_path": "adr/activity-core-ops-runs-vs-work-records/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ops-warden-adr-0001",
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"canonical_path": "adr/ops-warden-catalog-pointer-layer/v1/index.html",
"revision_path": "adr/ops-warden-catalog-pointer-layer/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ops-warden-adr-0002",
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0002-conduit-not-broker.md",
"canonical_path": "adr/ops-warden-conduit-not-broker/v1/index.html",
"revision_path": "adr/ops-warden-conduit-not-broker/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ops-warden-adr-0003",
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"canonical_path": "adr/ops-warden-cover-gaps/v1/index.html",
"revision_path": "adr/ops-warden-cover-gaps/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ops-warden-adr-0004",
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"canonical_path": "adr/ops-warden-high-risk-read-boundary/v1/index.html",
"revision_path": "adr/ops-warden-high-risk-read-boundary/v1/revisions/{revision}/index.html",
"review_interval": "6m"
},
{
"id": "ops-warden-adr-0005",
"source_repo": "ops-warden",
"source_path": "docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"canonical_path": "adr/ops-warden-implement-narrowly/v1/index.html",
"revision_path": "adr/ops-warden-implement-narrowly/v1/revisions/{revision}/index.html",
"review_interval": "6m"
}
]
}

View file

@ -56,6 +56,11 @@
"remote": "https://forgejo.coulomb.social/coulomb/markitect-main.git",
"selectors": ["docs/adr/*.md"]
},
"ops-warden": {
"branch": "main",
"remote": "https://forgejo.coulomb.social/coulomb/ops-warden.git",
"selectors": ["docs/adr/*.md"]
},
"net-kingdom": {
"branch": "main",
"remote": "https://forgejo.coulomb.social/coulomb/net-kingdom.git",

View file

@ -2,32 +2,32 @@
"schema_version": 1,
"sources": [
{
"disposition": "metadata-pending",
"reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/adr-001-event-bridge-architecture.md",
"source_repo": "activity-core"
},
{
"disposition": "metadata-pending",
"reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/adr-002-definition-format.md",
"source_repo": "activity-core"
},
{
"disposition": "metadata-pending",
"reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/adr-003-rule-instruction-model.md",
"source_repo": "activity-core"
},
{
"disposition": "metadata-pending",
"reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/adr-004-producer-trust-boundary.md",
"source_repo": "activity-core"
},
{
"disposition": "metadata-pending",
"reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/adr-005-ops-runs-vs-dev-work-records.md",
"source_repo": "activity-core"
},
@ -343,6 +343,42 @@
"source_path": "docs/architecture/net-kingdom_v0.1.md",
"source_repo": "net-kingdom"
},
{
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/ADR-0001-catalog-is-a-pointer-layer.md",
"source_repo": "ops-warden"
},
{
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/ADR-0002-conduit-not-broker.md",
"source_repo": "ops-warden"
},
{
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md",
"source_repo": "ops-warden"
},
{
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md",
"source_repo": "ops-warden"
},
{
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",
"source_path": "docs/adr/ADR-0005-implement-narrowly-route-broadly.md",
"source_repo": "ops-warden"
},
{
"disposition": "excluded",
"reason": "Directory index, not an architecture decision record.",
"source_path": "docs/adr/README.md",
"source_repo": "ops-warden"
},
{
"disposition": "published",
"reason": "Published through an explicit publication.json document entry.",

View file

@ -150,7 +150,7 @@ and is not published until the architecture selector and inventory catch up.
```task
id: POLICY-NEXUS-WP-0002-T02
status: todo
status: done
priority: high
```
@ -168,11 +168,14 @@ Implementation lands in `markitect-main`. If that is more than a small PR,
spawn a Markitect workplan and depend on it. Do not reimplement validation
here.
Completed 2026-08-19. `markitect/schemas/arc42-schema-v1.0.md` and catalog
id `arc42-v1`. All five first-wave documents and the template validate.
### T03 — Infospace index
```task
id: POLICY-NEXUS-WP-0002-T03
status: todo
status: done
priority: high
```
@ -192,6 +195,11 @@ architecture", using arc42 as the discipline.
Depends on T01. Can start as soon as the estate document path is fixed,
even as a stub.
Completed 2026-08-19. Pointer infospace at
`the-custodian/canon/architecture/infospace/` — five system entities,
arc42 discipline, no copied document bodies. `markitect infospace check`
runs.
### T04 — First document: Coulomb estate
```task

View file

@ -268,6 +268,10 @@ ADR-0001 (with publication front-matter) and railiance-master
ADR-00010008. Inventory dispositions match. Remaining `publish` rows
wait on later packets.
2026-08-19: activity-core ACT-ADR-001005 (metadata applied in the
owning repo) and ops-warden ADR-00010005 (harvest request; already
publication-ready) are registered.
### T07 — Point the architecture documents at what remains
```task