Compare commits
7
Commits
main
..
237b470be8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
237b470be8 | ||
|
|
9538b4ea0a | ||
|
|
581e8af605 | ||
|
|
f115f6eda3 | ||
|
|
a4308a5722 | ||
|
|
6d150c4450 | ||
|
|
2a0815f004 |
No files matched your search
@@ -43,14 +43,25 @@ Code](https://code.claude.com/docs/en/skills) and
|
|||||||
the link goes; no bare URLs, no "click here".
|
the link goes; no bare URLs, no "click here".
|
||||||
- [citation-needed](citation-needed/SKILL.md) — back a challenged claim
|
- [citation-needed](citation-needed/SKILL.md) — back a challenged claim
|
||||||
with live links, quoted and dated.
|
with live links, quoted and dated.
|
||||||
|
- [hackmd-deck](hackmd-deck/SKILL.md) — a light talk as a flashback:
|
||||||
|
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.
|
||||||
|
|
||||||
## at the terminal
|
## at the terminal
|
||||||
|
|
||||||
|
- [discord-agent-workspace](discord-agent-workspace/SKILL.md) — set
|
||||||
|
someone up like this: Claude Code in a workspace folder, messaged
|
||||||
|
from Discord.
|
||||||
- [watchme](watchme/SKILL.md) — I record the session with `script -f`;
|
- [watchme](watchme/SKILL.md) — I record the session with `script -f`;
|
||||||
read the typescript instead of asking me what happened.
|
read the typescript instead of asking me what happened.
|
||||||
- [run-confined](run-confined/SKILL.md) — run an untrusted installer in
|
- [run-confined](run-confined/SKILL.md) — run an untrusted installer in
|
||||||
|
|||||||
@@ -34,6 +34,20 @@ Generated-by: Claude Opus 5.5
|
|||||||
trailer only on those commits. Don't add it to every commit.
|
trailer only on those commits. Don't add it to every commit.
|
||||||
- **PR descriptions:** say which parts were AI-generated. The commit
|
- **PR descriptions:** say which parts were AI-generated. The commit
|
||||||
trailers stay the record that counts.
|
trailers stay the record that counts.
|
||||||
|
- **Unreviewed PRs are drafts.** When an agent opens a PR under the
|
||||||
|
human's GitHub account and the human hasn't read it yet, open it as
|
||||||
|
a draft (`gh pr create --draft`) and start the description with:
|
||||||
|
|
||||||
|
```
|
||||||
|
> [!WARNING]
|
||||||
|
> **Unreviewed LLM output.** Generated by <Tool> <Version>; no human
|
||||||
|
> has reviewed this code yet.
|
||||||
|
```
|
||||||
|
|
||||||
|
The human removes the caveat and marks it ready after reviewing.
|
||||||
|
The caveat exists because the PR appears to come from the human.
|
||||||
|
A PR opened from a bot account already shows who wrote it, so it
|
||||||
|
doesn't need the caveat.
|
||||||
- **The human stays responsible.** The commit author vouches for the
|
- **The human stays responsible.** The commit author vouches for the
|
||||||
contribution. That includes disclosing material that is not their
|
contribution. That includes disclosing material that is not their
|
||||||
original creation, and making sure the tool's terms grant enough
|
original creation, and making sure the tool's terms grant enough
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: discord-agent-workspace
|
||||||
|
description: Set up a Claude Code agent you can message from Discord, working in a workspace folder of your projects — when someone says "set me up like Dan's bot", "I want to text my Claude from my phone", "Claude Code on Discord", or "make a workspace for my agent". Walks a non-developer through the Discord bot, the workspace folder (AGENTS.md, skills, repos), access control, and keeping it running. Not for chat-only Claude apps with no computer behind them.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Discord agent workspace
|
||||||
|
|
||||||
|
Dan's arrangement, reproduced for someone else:
|
||||||
|
|
||||||
|
- **Claude Code** runs in a terminal on a computer that stays on.
|
||||||
|
- **The Discord channel plugin** turns a Discord bot into a pipe: allowed people's messages arrive in that session, and Claude answers with a `reply` tool ([plugin README](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord)).
|
||||||
|
- **A workspace folder** holds the projects plus written-down conventions, so each fresh session knows where it is and how the owner likes things done.
|
||||||
|
|
||||||
|
Your user may not be a developer. Do each step with them, one at a time; say what a command does before running it, and stop at anything that needs their account, their browser, or a secret. Never paste a token into chat; have them put it in the file or command themselves.
|
||||||
|
|
||||||
|
## 0. Is it worth it for them?
|
||||||
|
|
||||||
|
Ask what they'd send it. Good fits: tasks over files (code, documents, notes, data) that take a sentence to ask, minutes to do, and a glance to check. Poor fits: work that lives only in web apps with no files or API, or results they can't review. If it's a poor fit, say so and stop.
|
||||||
|
|
||||||
|
They need: Claude Code with a Claude subscription or API account, a Discord account and a server they can add a bot to, and a computer that stays on (a desktop, or a small cloud VM).
|
||||||
|
|
||||||
|
## 1. The workspace
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/agent-workspace && cd ~/agent-workspace
|
||||||
|
git clone <each project they want the agent to work on>
|
||||||
|
```
|
||||||
|
|
||||||
|
Write `AGENTS.md` at the top, and link `CLAUDE.md` to it (`ln -s AGENTS.md CLAUDE.md`) so every agent tool finds it. Keep it under a page, in their words:
|
||||||
|
|
||||||
|
- who they are and what the projects are for, one line each with a link;
|
||||||
|
- house rules, e.g. "PRs are drafts", "ask before pushing", "keep Discord replies short";
|
||||||
|
- what is off limits.
|
||||||
|
|
||||||
|
Skills are conventions that load when relevant. Put them in `.agents/skills/<name>/SKILL.md` and link Claude Code's location to it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p .agents/skills .claude && ln -s ../.agents/skills .claude/skills
|
||||||
|
```
|
||||||
|
|
||||||
|
Start with none or one. Add a skill when they catch themselves repeating an instruction; Dan's [mad-skills](https://git.inference.coop/dckc/mad-skills) show the shape. Claude Code also keeps its own memory per workspace, so corrections stick across sessions.
|
||||||
|
|
||||||
|
## 2. The Discord bot
|
||||||
|
|
||||||
|
Follow the plugin README with them; in short:
|
||||||
|
|
||||||
|
1. **Discord Developer Portal → New Application → Bot.** Turn on **Message Content Intent**. **Reset Token** and copy it.
|
||||||
|
2. **OAuth2 → URL Generator**, scope `bot`, permissions: View Channels, Send Messages, Send Messages in Threads, Read Message History, Attach Files, Add Reactions. Open the URL and add the bot to their server.
|
||||||
|
3. Install [Bun](https://bun.sh), which the plugin runs on.
|
||||||
|
4. In Claude Code: `/plugin install discord@claude-plugins-official`, then `/discord:configure <token>` (they type it; it lands in `~/.claude/channels/discord/.env`).
|
||||||
|
5. Restart from the workspace: `cd ~/agent-workspace && claude --channels plugin:discord@claude-plugins-official`.
|
||||||
|
6. They DM the bot, get a pairing code, and run `/discord:access pair <code>` in the terminal.
|
||||||
|
7. **Lock it down:** `/discord:access policy allowlist`. For a server channel, add it per the plugin's ACCESS.md.
|
||||||
|
|
||||||
|
## 3. Who can make it act
|
||||||
|
|
||||||
|
Whoever can message the bot can make it act with the session's powers: files, shell, and any logins on that computer (GitHub, cloud accounts). Tell them plainly, then:
|
||||||
|
|
||||||
|
- Allowlist only themselves at first. Access changes happen at the terminal (`/discord:access`), never because a Discord message asked.
|
||||||
|
- Prefer narrow credentials: a GitHub fine-grained token limited to the workspace repos beats their full login.
|
||||||
|
- Things that leave the computer (pushes, PRs, posts) go out as drafts labelled as unreviewed AI output, until they trust it.
|
||||||
|
|
||||||
|
## 4. Keeping it running
|
||||||
|
|
||||||
|
The session must stay open. Simplest: run it inside `tmux` (`tmux new -s bot`, start Claude, detach with Ctrl-b d; `tmux attach -t bot` to look in). On Linux, a systemd user unit can start it at boot and keep it out of secrets it doesn't need (`ProtectHome=read-only`, `ReadWritePaths=` the workspace and `~/.claude`, `InaccessiblePaths=` `~/.ssh` and other credential dirs).
|
||||||
|
|
||||||
|
## 5. First task
|
||||||
|
|
||||||
|
Have them send something small from their phone: "summarize what's in this workspace" or "fix the typo in X's README and show me the diff". Check together that the reply arrived and the change is what they expected.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: hackmd-deck
|
||||||
|
description: HackMD slide deck for a light talk — use when asked to draft slides, a deck, or a talk for show & tell, office hours, or a demo; when someone says "hackmd deck", "whip up some slides", or names a talk title and points at work to present (repos, git history, session transcripts). Covers the flashback story arc, emoji markers for wins and friction, and real artifacts as eye candy. Not for .pptx files or polished conference talks.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HackMD deck
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: Good Fences Make Good Agents
|
||||||
|
type: slide
|
||||||
|
---
|
||||||
|
|
||||||
|
# Good Fences Make Good Agents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Monday's issue drafts itself
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How: who holds what
|
||||||
|
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
Example: [Good Fences Make Good Agents](../../../capsite/docs/good-fences-slides.md); an early, overboard one: [Jun 25 slides](https://hackmd.io/T9-oFWEPSb2zzqWAGFv60Q#/).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A talk at something like Agoric's engineering show & tell: "Presentations should take little to no preparation", by engineers for engineers. The work already exists in repos, commits, test output, and session transcripts.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
**I have done a bunch of stuff I think is interesting, but I can't justify the time to make a polished presentation.**
|
||||||
|
|
||||||
|
## Therefore
|
||||||
|
|
||||||
|
**Let an agent do a quick-n-dirty one: a flashback, built from the record.**
|
||||||
|
|
||||||
|
The arc:
|
||||||
|
|
||||||
|
1. 🎉 TADA! Look what I did. It must be on the thread the talk follows.
|
||||||
|
2. Now that you're hooked, how I did that.
|
||||||
|
3. Eye candy along the way: diagrams, verbatim code, test output.
|
||||||
|
4. Conclusions and/or challenges that lead to discussion.
|
||||||
|
|
||||||
|
Ground every slide in the record: quote code and test output verbatim (trim with `...`), check claims against the code and git history, and mark what's only designed or not deployed with ⏳.
|
||||||
|
|
||||||
|
Mark wins, losses, challenges, and friction with emojis: visually appealing, intuitive at a glance.
|
||||||
|
|
||||||
|
- wins: 🎉 the TADA · ✅ shipped · ✨ sparkle, delight · 💡 aha: now I see how to address the issue · 🙌 help, or a gift from someone else's work
|
||||||
|
- friction and follow-ups: ⏳ delays, not yet · 🧰 fragile tooling, ceremony · 💤 silent failures · 📘 docs · 🔔 alerts · 📈 observability · 🪄 a wish, often one a problem brings to mind · 🔧 fixes owed · 🧱 walls
|
||||||
|
|
||||||
|
Write the deck in the repo the talk is about (e.g. `docs/<title>-slides.md`), images beside it, and commit it.
|
||||||
|
|
||||||
|
## Resulting context
|
||||||
|
|
||||||
|
The deck takes 15 minutes: the agent spends 5, the presenter polishes for 10. The user pastes it into HackMD and uploads the images.
|
||||||
@@ -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