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.
|
TADA, then how, then the challenges, tagged for an emoji retro.
|
||||||
- [ibis-design](ibis-design/SKILL.md) — search a design space:
|
- [ibis-design](ibis-design/SKILL.md) — search a design space:
|
||||||
issues, positions, and arguments.
|
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
|
- [obsidian-markdown](obsidian-markdown/SKILL.md) — the workspace is
|
||||||
an Obsidian vault: one line per paragraph, relative links, no file
|
an Obsidian vault: one line per paragraph, relative links, no file
|
||||||
symlinks you expect Obsidian to show.
|
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