Design: grip whiteboard¶
| Status | Milestones 1 and 2 built in 0.2.0; milestone 3 open |
| Target | 0.2.0 |
| Prototype | Open the clickable prototype (one example diff, simulated grading) |
The problem¶
Today grip asks five questions in a terminal, one after another. That works, but it misses three things the whiteboard defense needs:
- The picture. At a whiteboard you draw the system before you defend it. A terminal quiz never shows how the parts of a change connect, so questions about trust boundaries and data flow are hard to ask and hard to answer.
- Two of the four questions. "What happens if this actor behaves maliciously?" and "what data structure did you use here and why?" are not asked directly.
- A place to be pulled aside. There is no way for a reviewer to run the conversation in person, with the rubric in front of them.
What it does¶
grip whiteboardreads the diff, exactly likegrip quiz.- One model call returns a board: the parts of the change, how they connect, which inputs are untrusted, and five questions pinned to specific parts, each with a hidden rubric.
- grip serves the board on
127.0.0.1and opens it in the browser. - The developer clicks a part, reads the question pinned there, answers in their own words. Rubrics stay on the server.
- "Hand in" sends the answers to grip, which grades them with the same provider and the same 100-point scale as the quiz. A pass is remembered for that diff, so the push hook lets it through.
Commands¶
| Command | What it does |
|---|---|
grip whiteboard |
Board for the staged changes, or every unpushed commit (same rules as grip quiz) |
grip whiteboard main..feat |
Board for a range |
grip whiteboard --interview |
Reviewer mode: rubrics visible, tick the points you hear, score at the end |
grip whiteboard --export board.html |
One static file with the board and questions, no rubrics, nothing graded |
grip whiteboard --no-open |
Print the URL instead of opening a browser (SSH, containers) |
/grip-whiteboard |
Skill: the agent starts the board and gives you the link; you answer in the browser |
Planned for milestone 3, one new key:
fallback would serve a board when the hook has no terminal (an IDE push button, an
agent's shell) instead of skipping. always would replace the terminal quiz.
Question kinds¶
Each pin has one kind. The first four are the whiteboard defense; the fifth keeps grip's existing testing question.
| Kind | Asks | Required when |
|---|---|---|
why this way |
Why X instead of Y; the model must name a real alternative Y | always |
malicious actor |
What a hostile caller, input or tenant can do | the board has an untrusted node |
data structure |
Why this representation, and what it costs | the diff adds or changes one |
where it fails |
The input, load or state that breaks it | always |
how you know |
How you would show it works | always |
how it works |
What the change does, in your words | fills a slot a kind above cannot |
Five pins, 20 points each, so the Grip Score and pass mark mean the same thing as today.
The board¶
class Node(BaseModel):
id: str # short slug, unique on the board
label: str # what a person would write on the whiteboard
role: str # one line
kind: Literal["component", "store", "external", "context"]
trust: Literal["trusted", "untrusted"]
files: list[str] # paths from the diff; empty only for external/context
class Edge(BaseModel):
source: str
target: str
label: str # what flows: "request + api_key"
crosses_trust: bool # derived by grip, not the model
class Pin(BaseModel):
on: str # node id or "source->target"
kind: PinKind
question: str
rubric: list[str] # 2 to 4 points, hidden from the developer
class Board(BaseModel):
summary: str
nodes: list[Node] # 2 to 8
edges: list[Edge]
pins: list[Pin] # exactly 5
Validation before anything is shown. Every edge and pin points at a real node. Every
component or store lists at least one file that is in the diff. Required kinds are
present. Labels are plain text under 40 characters. On failure grip asks the model once
more with the errors listed, then stops and points to grip quiz. A board that does not
validate is never served. Every field is required, with no defaults, so the schema also
works with providers that enforce strict structured output.
Layout is grip's job, not the model's. Columns by role: untrusted actors on the left, then the code in topological order of the edges, stores below what writes them, external services on top. Deterministic, so the same board always looks the same, and testable without a browser.
Serving¶
- Python standard library
ThreadingHTTPServer, bound to127.0.0.1on a random port. No new dependency. - Every URL carries a 128-bit random token:
http://127.0.0.1:PORT/TOKEN/. Requests without it get 404. - The
Hostheader must be127.0.0.1:PORT, andPOSTrequires a matchingOrigin. This blocks DNS rebinding and other sites posting to the server. - One page, all CSS and JS inline, a strict Content-Security-Policy with a hash for the one script, no network requests, system fonts with brand fallbacks.
- Routes:
GET /TOKEN/the page,GET /TOKEN/board.jsonthe board without rubrics,POST /TOKEN/answersgrade once and return the sheet. - The server exits after grading, on Ctrl-C, or after 20 idle minutes. Nothing is recorded until grading succeeds.
Defending this design¶
The same four questions, asked of this document.
Why a local web page and not a richer terminal UI? A diagram with clickable parts, multi-line answers and a reviewer view do not fit a terminal. A browser is on every developer machine and needs no new dependency. Why not a hosted service? The diff and the answers would leave the machine; today only the model provider sees them, and that stays true.
What if an actor behaves maliciously?
- Another website tries to read or post to the server: token in the path,
HostandOriginchecks, no CORS headers. - The diff contains a prompt injection that makes the model write HTML or script into a
label: every model string is inserted with
textContent, the CSP forbids inline script other than grip's own, and labels are validated as plain text. - The coding agent tries to answer for the developer: the answers are typed into the
browser and go straight to grip, so they never pass through the agent's conversation.
An agent with a shell can still read the address and post to it, exactly as it could
answer the terminal quiz; the
/grip-whiteboardskill forbids it.HostandOriginchecks stop browsers, not local programs. - A developer reads the rubric: while the board is open it lives only in grip's memory;
after grading it is in
last-report.json, as for the quiz. grip checks understanding; it is not an exam proctor.
What data structure, and why? A board is lists of nodes, edges and pins with string ids, not free-form Mermaid or SVG from the model. Ids let grip validate references, pin questions, and run its own layout. Free-form drawing output could not be checked, could carry markup, and would lay out differently every run.
Where does this fail?
| Situation | What happens |
|---|---|
| No browser (SSH, container) | --no-open prints the URL; forward the port, or use the terminal quiz |
| A very large diff | At most 8 nodes; the model groups files. Pins favour the riskiest parts |
| The model invents a component | Validation rejects a node with no files from the diff; one retry, then the terminal quiz |
| The browser is closed mid-way | Nothing is recorded; the server times out; the push stays blocked |
| A slow agent CLI provider | Same latency as the quiz today; the page shows progress while grading |
| A docs-only change | No untrusted node, no data structure: how it works fills the slots |
Milestones¶
M1: the board (0.2.0)
-
Boardmodels, prompt, validation, one retry -
generate_boardon every provider; scripted boards for thefakeprovider - Layered layout in Python, with tests
-
grip whiteboard --export(static, no rubrics)
M2: defend it (0.2.0)
- Local server, token,
Host/Originchecks, CSP, idle timeout - Defend mode: answer, hand in, grade, remember the pass
- Interview mode
-
/grip-whiteboardskill and docs - Feature clip
M3: everywhere (0.3.0)
-
whiteboard = "fallback"in the hook for pushes without a terminal - Boards for Keep A Grip practice problems
Decisions¶
Taken as proposed for 0.2.0; each can be revisited.
- Should a whiteboard pass satisfy the push hook? Yes, same score and pass mark.
- Should
--exportever include rubrics, for a reviewer who runs the session elsewhere? No; use--interviewon the reviewer's machine. - Five pins always, or four to six by diff size? Five, to keep scores comparable.
- Brand fonts embedded in the binary (about 150 KB) or system fonts? System fonts.