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:
1 parent
9538b4ea0a
commit
237b470be8
3 files changed
+257
No files matched your search
@@ -47,6 +47,12 @@ Code](https://code.claude.com/docs/en/skills) and
|
||||
TADA, then how, then the challenges, tagged for an emoji retro.
|
||||
- [ibis-design](ibis-design/SKILL.md) — search a design space:
|
||||
issues, positions, and arguments.
|
||||
- [wiring-diagram](wiring-diagram/SKILL.md) — who holds what, drawn
|
||||
like MarkM's ERTP slides: every arrow cites the code, and Mallory
|
||||
shows what a leak doesn't reach.
|
||||
TODO: consider copying or adapting its companion for message order,
|
||||
sequence-diagram-actor-sim (agoric-sdk `.agents/skills/` on branch
|
||||
`connolly/ago-1299-agentic-planning-tdd`).
|
||||
- [obsidian-markdown](obsidian-markdown/SKILL.md) — the workspace is
|
||||
an Obsidian vault: one line per paragraph, relative links, no file
|
||||
symlinks you expect Obsidian to show.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
name: wiring-diagram
|
||||
description: >-
|
||||
Capability wiring diagram, "who holds what" — use when asked for a wiring diagram, a picture of who holds what, a capability diagram, or "a diagram like the cookie-rotation one"; when reviewing a design for POLA and wanting to see the reference graph. MarkM ERTP-slide style: actors are balls, makers pale regions, objects rounded squares, state ledger tables, references dot-to-arrowhead; HTML/SVG plus a headless-Chromium PNG. Not for message order over time: that's a sequence diagram (sequence-diagram-actor-sim in agoric-sdk).
|
||||
---
|
||||
|
||||
# Capability wiring diagram
|
||||
|
||||
One picture that answers, for a design: **who holds a reference to what,
|
||||
and therefore who can do what.** It complements a sequence diagram,
|
||||
which shows *when* messages flow. This one shows the standing graph of
|
||||
authority that makes those messages possible. For sequences, see
|
||||
`~/projects/ymax-workspace/agoric-sdk/.agents/skills/sequence-diagram-actor-sim/SKILL.md`
|
||||
(on agoric-sdk branches `connolly/ago-1264-agentic-planning` and
|
||||
`connolly/ago-1299-agentic-planning-tdd`, not yet on master).
|
||||
|
||||
Exemplars:
|
||||
- `~/oros-bots/capsite-cookie-rotation/docs/cookie-rotation-wiring.html`
|
||||
(+ `.png`): the original.
|
||||
- `~/qtrx/packages/umb-download/docs/wiring.html` (+ `.png`): three
|
||||
makers, two outside parties, and an adversary holding a webkey.
|
||||
|
||||
Start from [`template.html`](template.html), next to this file. It has
|
||||
the drawing helpers and a small example scene to replace.
|
||||
|
||||
## Read the graph from the code, not the story
|
||||
|
||||
Every arrow must correspond to something real: a constructor argument, a
|
||||
stored id, a binding, a held token, or a file someone keeps. Before
|
||||
drawing, list the references as `holder → target: where in the code`.
|
||||
Read the factories (`make…`, `newX`, `configure`) and anything that
|
||||
stores an id: a DO id held as a string *is* a reference. If you can't
|
||||
cite where a reference comes from, it doesn't get an arrow.
|
||||
|
||||
Watch for references that are dropped on purpose (e.g. a setup route
|
||||
that mints an instance, hands its id to one holder and forgets it).
|
||||
That's often the point of the design. Say so in a caption, or just
|
||||
leave the arrow out.
|
||||
|
||||
## Vocabulary
|
||||
|
||||
| element | helper | means |
|
||||
|---|---|---|
|
||||
| blue ball | `actor(cx, cy, label)` | a person or a program with its own agency |
|
||||
| grey ball | `actor(cx, cy, label, "url(#ext)")` | outside the system: a browser extension, a third-party service |
|
||||
| pale region | `maker(x, y, w, h, "makeFoo")` | a maker (factory, DO class) and the instances it made; the stacked inner panes say "there are many" |
|
||||
| rounded square | `obj(x, y, w, h, label, stacked)` | an object or facet; `stacked` when one is made per instance |
|
||||
| ledger table | `table(x, y, title, rows, [colW0, colW1])` | an object's state; rows are key/value; `rowY(i)` and `colX` give arrow anchors |
|
||||
| dot → arrowhead | `ref(x1, y1, x2, y2, bend)` | holder references target; `bend` (±0.05–0.3) curves around things |
|
||||
| speech bubble | the `path` + `code` block | the one call the picture is about, in `E(target).method(args)` form |
|
||||
| caption | `cap(x, y, text, attrs)` | what an actor holds, and what it can't reach |
|
||||
|
||||
Facets are separate objects (`store` vs `readOnly`): drawing them apart
|
||||
is how the picture shows attenuation.
|
||||
|
||||
## Include an adversary
|
||||
|
||||
Add a Mallory who holds whatever is most likely to leak (an instance
|
||||
id, a webkey, a URL), with an arrow to exactly what it reaches. The
|
||||
caption says what she *can't* reach. Absent arrows are the security
|
||||
argument, and Mallory makes them visible.
|
||||
|
||||
## Layout
|
||||
|
||||
- Actors in a row along the top (y≈200), makers below (y≈320 onward),
|
||||
captions along the bottom (y≈ height − 14).
|
||||
- Put each maker under the actors that mostly use it, so arrows run
|
||||
short and downward.
|
||||
- Speech bubble at the top, its tail pointing at the speaker. Move the
|
||||
bubble's path (`M x,y`) and the tail segment (`h… l-38,48 l6,-48`)
|
||||
together.
|
||||
- Captions: two or three short lines, never on top of an arrow or a
|
||||
ball. Use `"text-anchor": "start"` to set one beside a ball.
|
||||
- Long arrows that cross a maker: increase `|bend|` until the curve
|
||||
clears tables and objects. The control point is the midpoint offset
|
||||
by `bend × (−dy, dx)`.
|
||||
|
||||
## Accessibility
|
||||
|
||||
Keep `<title>` short. Write `<desc>` as prose that names every actor and
|
||||
maker, every reference, the bubble's call, and what the adversary
|
||||
can't reach. Someone who can't see the picture should get the same
|
||||
argument from it.
|
||||
|
||||
## Render and check
|
||||
|
||||
The page is self-contained (no network). Render a PNG with headless
|
||||
Chromium. The viewport comes out shorter than `--window-size`, so render
|
||||
taller and crop to the SVG size:
|
||||
|
||||
```sh
|
||||
chromium --headless=new --disable-gpu --no-sandbox --hide-scrollbars \
|
||||
--window-size=W,$((H+100)) --screenshot=full.png "file://$PWD/NAME.html"
|
||||
python3 -c "from PIL import Image; Image.open('full.png').crop((0,0,W,H)).save('NAME.png')" && rm full.png
|
||||
```
|
||||
|
||||
Then **look at the PNG** (Read it) and fix overlaps: captions on arrows,
|
||||
arrows through tables, text clipped at the bottom. Expect two or three
|
||||
rounds. Commit the HTML (source) and the PNG separately, so the source
|
||||
diff stays readable.
|
||||
|
||||
## Review checklist
|
||||
|
||||
- Does every arrow cite something in the code? Is any real reference
|
||||
missing?
|
||||
- Does each actor hold only what its job needs (POLA)? If the picture
|
||||
shows an actor holding more, that's a finding about the design, not a
|
||||
drawing problem.
|
||||
- Are facets drawn apart where they differ in authority?
|
||||
- Does the adversary's caption match what the arrows show?
|
||||
- Does `<desc>` tell the same story in prose?
|
||||
- Is any secret value drawn? Elide values (`session=…`, `{…}`): the
|
||||
picture shows *that* a row exists, not its contents.
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user