wiring-diagram: who holds what, drawn like MarkM's ERTP slides

A capability wiring diagram as a self-contained HTML/SVG page plus a
headless-Chromium PNG. Every arrow has to point to something in the
code, and a Mallory holding what's likeliest to leak shows what that
doesn't reach. template.html has the drawing helpers, adapted from
capsite's cookie-rotation-wiring.

README: a TODO to consider adapting agoric-sdk's
sequence-diagram-actor-sim as its companion.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-11 14:22:44 -05:00
1 parent 9538b4ea0a
commit 237b470be8
3 files changed
+257

No files matched your search

+138
View File
@@ -0,0 +1,138 @@
<!doctype html>
<html><head><meta charset="utf-8">
<title>TITLE: who holds what</title>
<!-- drawn after the style of MarkM's ERTP slides; NAME.png is a headless-Chromium screenshot of this page -->
<style>
html, body { margin: 0; background: #fff; }
.sans { font-family: "Helvetica Neue", "DejaVu Sans", sans-serif; }
.obj { font-family: "DejaVu Serif", serif; font-style: italic; }
.mono { font-family: "DejaVu Sans Mono", monospace; }
</style></head>
<body>
<svg id="c" xmlns="http://www.w3.org/2000/svg" width="1120" height="820" viewBox="0 0 1120 820"
role="img" aria-labelledby="title desc">
<title id="title">TITLE: who holds what</title>
<desc id="desc">Prose for screen readers: name every actor and maker, every
reference (who holds what), the speech bubble's call, and what the
adversary can and cannot reach.</desc>
<defs>
<radialGradient id="ball" cx="40%" cy="35%" r="70%">
<stop offset="0%" stop-color="#7cc8f8"/>
<stop offset="100%" stop-color="#3aa6ee"/>
</radialGradient>
<radialGradient id="ext" cx="40%" cy="35%" r="70%">
<stop offset="0%" stop-color="#e3e3e3"/>
<stop offset="100%" stop-color="#b9b9b9"/>
</radialGradient>
<linearGradient id="objfill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6cbcf4"/>
<stop offset="100%" stop-color="#4aa8ec"/>
</linearGradient>
<marker id="head" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#1d4f7a"/>
</marker>
</defs>
</svg>
<script>
const svg = document.getElementById("c");
const NS = "http://www.w3.org/2000/svg";
const el = (name, attrs, parent = svg) => {
const e = document.createElementNS(NS, name);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
parent.appendChild(e);
return e;
};
const text = (x, y, s, cls, attrs = {}) => {
const t = el("text", { x, y, class: cls, "text-anchor": "middle", fill: "#1d3550", ...attrs });
t.textContent = s;
return t;
};
const INK = "#1d4f7a";
const dot = (x, y) => el("circle", { cx: x, cy: y, r: 4.5, fill: INK });
/** a reference: dot at the holder, arrowhead at the target */
const ref = (x1, y1, x2, y2, bend = 0, extra = {}) => {
dot(x1, y1);
const mx = (x1 + x2) / 2 - (y2 - y1) * bend, my = (y1 + y2) / 2 + (x2 - x1) * bend;
el("path", { d: `M${x1},${y1} Q${mx},${my} ${x2},${y2}`, fill: "none", stroke: INK, "stroke-width": 2, "marker-end": "url(#head)", ...extra });
};
// maker box: a pale rounded region, with stacked instances inside
function maker(x, y, w, h, label) {
el("rect", { x, y, width: w, height: h, rx: 26, fill: "#d6ecfb", stroke: "#b5dbf5", "stroke-width": 1.5 });
text(x + 18, y + 30, label, "obj", { "text-anchor": "start", "font-size": 20 });
for (let i = 2; i >= 0; i -= 1) {
el("rect", {
x: x + 18 + i * 9, y: y + 48 + i * 9, width: w - 52, height: h - 82, rx: 22,
fill: "#c8e5fa", stroke: "#8ec9f2", "stroke-width": 1.5, "fill-opacity": 0.9,
});
}
}
// an object: rounded square, optionally stacked (several made)
function obj(x, y, w, h, label, stacked = false) {
const n = stacked ? 3 : 1;
for (let i = n - 1; i >= 0; i -= 1) {
el("rect", { x: x + i * 7, y: y - i * 7, width: w, height: h, rx: 12, fill: "url(#objfill)", stroke: "#2f8fd6", "stroke-width": 1.5 });
}
text(x + w / 2, y + h / 2 + 7, label, "obj", { "font-size": 19 });
}
function actor(cx, cy, label, fill = "url(#ball)") {
el("circle", { cx, cy, r: 52, fill, stroke: "#2f8fd6", "stroke-width": 1.5 });
text(cx, cy + 6, label, "sans", { "font-size": label.length > 8 ? 15 : 17 });
}
// a table like the ERTP ledger
function table(x, y, title, rows, colW) {
const rowH = 28, w = colW[0] + colW[1];
el("rect", { x, y, width: w, height: 36 + rowH * rows.length, fill: "#5fb5f2", stroke: "#2f8fd6", "stroke-width": 1.5 });
text(x + w / 2, y + 25, title, "obj", { "font-size": 18 });
rows.forEach(([k, v], i) => {
const ry = y + 36 + i * rowH;
el("line", { x1: x, y1: ry, x2: x + w, y2: ry, stroke: "#2f8fd6", "stroke-width": 1.2 });
text(x + 10, ry + 19, k, "mono", { "text-anchor": "start", "font-size": 14 });
if (v) text(x + colW[0] + 10, ry + 19, v, "mono", { "text-anchor": "start", "font-size": 14 });
});
el("line", { x1: x + colW[0], y1: y + 36, x2: x + colW[0], y2: y + 36 + rowH * rows.length, stroke: "#1d4f7a", "stroke-width": 1.5 });
return { rowY: (i) => y + 36 + i * rowH + rowH / 2, x, w, colX: x + colW[0] };
}
// ---- scene: replace from here down ----
// speech bubble: the one invocation the picture is about; tail toward the speaker
el("path", {
d: "M30,22 h470 a16,16 0 0 1 16,16 v34 a16,16 0 0 1 -16,16 h-330 l-38,48 l6,-48 h-102 a16,16 0 0 1 -16,-16 v-34 a16,16 0 0 1 16,-16 z",
fill: "#d9d9d9", stroke: "#555", "stroke-width": 2,
});
const code = el("text", { x: 46, y: 62, class: "mono", "font-size": 17, fill: "#222" });
code.innerHTML = '<tspan fill="#b0177b">await</tspan> E(store).set(<tspan fill="#1a7f37">"cookie"</tspan>, freshCookie);';
// actors: people, programs, outside services (grey: "url(#ext)")
actor(150, 200, "Alice");
actor(560, 200, "client");
actor(930, 200, "Mallory");
// makers, with what they made inside
maker(30, 320, 470, 470, "makeMapStore");
maker(560, 320, 530, 470, "makeWebApp");
obj(80, 420, 120, 80, "store");
obj(80, 600, 120, 80, "readOnly", true);
const kv = table(260, 430, "kv", [["cookie", "session=…"], ["", ""], ["", ""]], [90, 110]);
obj(620, 560, 140, 90, "access", true);
const init = table(830, 420, "init", [["url", "…"], ["headerStore", ""]], [130, 85]);
// references: holder first; one comment per arrow, in the form "holder → target"
ref(140, 228, 125, 418, 0.05); // Alice → store
ref(172, 224, 508, 210, 0.04); // Alice → client (she wired it)
ref(554, 218, 660, 553, 0.12); // client → access
ref(916, 220, 720, 553, -0.08); // Mallory → access
ref(160, 470, kv.x - 2, kv.rowY(0), 0.05); // store → kv
ref(150, 666, kv.x - 2, kv.rowY(0) + 6, -0.25); // readOnly → kv
ref(760, 600, init.x - 2, init.rowY(1), -0.1); // access → init
ref(init.colX + 40, init.rowY(1), 208, 640, -0.28); // init.headerStore → readOnly
// captions: what each actor holds, what the adversary can't reach
const cap = (x, y, s, attrs = {}) => text(x, y, s, "sans", { "font-size": 14, fill: "#33536f", ...attrs });
cap(930, 118, "holds the instance id:");
cap(930, 136, "uses the cookie, can't reach store");
cap(265, 806, "shapes: keyShape \"cookie\", valueShape M.string()");
</script>
</body></html>