01Safety
0.0 / 30
What changed in the harness
Selection accuracy 100→100, token cost down 0%, unconfirmed writes 0%→0%.
Category breakdown
Earned points across the four signals Gradable measures. Safety and Legibility are scored out of 30; Economics and Discoverability are scored out of 20.
0.0 / 30
25.6 / 30
7.1 / 20
10.7 / 20
Highest-impact fix
Estimated gain +30 pointsExpose machine-readable principal/tenant confirmation and a non-mutating permission check so agents can verify both before destructive actions.
Description evidence
9 defects found across the exposed tool descriptions. Suggested rewrites make purpose, inputs, boundaries, and returns easier for an agent to understand.
| Tool | Defect types | Suggested rewrite |
|---|---|---|
move_node |
no_return_description |
Move a node to a new parent, reorder it among siblings, or rename it without recreating it. Preserves IDs, bound variables, and component instances across the move, so callers tracking the node by ID never need to re-discover it. Use for: (a) changing child order within a container, (b) moving a subtree into a different parent, (c) fixing a placement mistake after jsx. Returns { data: { id, name, parent, index } } confirming the node's new location, name, and sibling position. Examples: move_node({node: "1:3", name: "NewTitle"}) — rename in place move_node({node: "1:3", parent: "1:4"}) — move into parent 1:4 move_node({node: "1:5", index: 0}) — reorder within current parent |
clone_node |
no_return_description |
Deep-copy a node with optional property overrides, producing an independent duplicate (not linked to the source). Returns { data: { nodeId, name, parent } } identifying the newly created clone and where it was placed. Examples: clone_node({node: "1:2"}) — clone to page root, same name clone_node({node: "1:2", parent: "/"}) — clone to page root explicitly clone_node({node: "1:2", parent: "/", name: "Hero Copy"}) — clone to root with custom name clone_node({node: "1:2", parent: "1:4"}) — clone into parent node 1:4 clone_node({node: "1:2", parent: "1:4", overrides: {"bg": "#D9D9D9"}}) |
set_variable_mode |
no_return_description |
Set a node to use a specific mode of a variable collection. This controls which variable values the node displays. For example, set a frame to use "Dark" mode of the "Theme" collection so all bound variables show dark values. Modes are set on the consumer node, not on the variable itself. Returns { data: { ok: true } } on success. Examples: set_variable_mode({node: "1:2", collection: "VariableCollectionId:1:2", mode: "1:1"}) set_variable_mode({node: "1:5", collection: "VariableCollectionId:1:3", mode: "1:2"}) |
add_component_prop |
params_unexplained |
Add a component property to a COMPONENT or COMPONENT_SET. Four supported types: TEXT — overridable string per instance (button label). Auto-binds to a text child via "bind". BOOLEAN — show/hide a layer per instance (icon toggle). Auto-binds to a node's visibility. INSTANCE_SWAP — swap a nested instance per instance (icon family, avatar). Auto-binds to an instance child. Optional "preferredValues" suggests swap candidates. SLOT — open slot filled at instance use site (composable Card body, Modal content). Optional "description" hints what belongs in the slot; optional "preferredValues" suggests content. No "bind" (SLOT is not tied to one child). VARIANT axes (Size, State, Theme) are NOT added here. Figma derives variant axes from the names of the variant components — create one COMPONENT per combination and use combine_components with a `variants` mapping. Calling this tool with type:"VARIANT" returns a redirect, so the optional `variantOptions` (an array of axis-option strings) is rarely used directly here. Auto-binding (TEXT/BOOLEAN/INSTANCE_SWAP): if "bind" omitted, the handler walks the component's children to match by name. SLOT skips binding (filled by consumers). preferredValues is `{type, key}[]` where `type` is "COMPONENT" or "COMPONENT_SET" and `key` is the PUBLISHED component key (component.key) — NOT a scene-graph node id. Use when: - Building a reusable component whose instances need per-use variation (label, icon, on/off state) - Defining a composable slot a consumer can fill (Card body, Modal content) Returns: { data: { message, nodeId, property, bound: boolean } } `bound` is false if auto-binding couldn't find a matching child — the property is still created, but instances won't override anything until you rerun with explicit "bind". Skip when: - Source is a FRAME — run create_component first; properties require COMPONENT or COMPONENT_SET - You want a VARIANT axis (Size, State) — use combine_components instead (see above) - Variation is one-off — edit the instance directly instead of templating Examples: add_component_prop({node: "1:2", name: "Label", type: "TEXT", default: "Click me", bind: "1:5"}) add_component_prop({node: "1:2", name: "Show Icon", type: "BOOLEAN", default: "true"}) add_component_prop({node: "1:2", name: "Icon", type: "INSTANCE_SWAP", default: "1:99", preferredValues: [{type: "COMPONENT", key: "abc123"}]}) add_component_prop({node: "1:2", name: "Footer", type: "SLOT", description: "Replace card footer"}) |
set_fill |
no_return_description |
Set fill or background color on a node. `fill` = text color or shape fill. `bg` = frame background. For stroke color, use `set_stroke`. Each call (single or batch item) needs `node` plus at least one of `fill`/`bg`. Batch by passing `nodes: [{node, fill?, bg?}, ...]`. Accepted color formats (for fill or bg): hex "#FFF", "#F5F5F5" gradient string CSS-like subset, not full CSS: "linear-gradient(<angle>deg, <#hex> <pos>%, ...)" "linear-gradient(to <direction>, ...)" directions: top/right/bottom/left + corners "radial-gradient(<stops>)" centered, no position/shape modifiers "radial-gradient(circle, <stops>)" circle shape only "conic-gradient(from <angle>deg, ...)" Rejected: "circle at X% Y%", "ellipse at ...", named colors, hsl(). variable token qualified bare name "$Surface/Card" transparent "transparent" (bg only) Returns: { data: { id, name, type, applied?, noop?, rejected? } } (Batch: { data: { count, results, errors?, partial? } }). `applied`/`noop` list prop keys; `rejected: [{key, reason}]` is TERMINAL (prop invalid for this node type / readonly) — do not retry it. If the only requested prop is rejected, returns `{ error }`. `partial: true` means some batch entries succeeded and some failed — check `errors[]` for which entries to retry. |
set_stroke |
no_return_description |
Set stroke (border) on a node. set_stroke({node: "1:2", stroke: "1 #E0E0E0"}) set_stroke({node: "1:2", stroke: "2 #333 inside"}) set_stroke({node: "1:2", color: "#E0E0E0", weight: 1, align: "inside"}) set_stroke({node: "1:2", color: "linear-gradient(90deg, #8B5CF6 0%, #F97316 100%)", weight: 1.5, align: "inside"}) // Batch — bulk stroke update in one call: set_stroke({nodes: [{node: "1:2", color: "#E0E0E0", weight: 1}, {node: "1:3", color: "#333", weight: 2}]}) Shorthand: "weight color align" (e.g. "1 #E0E0E0 inside"). Hex only in shorthand. Accepted color formats (for the explicit `color` field, not the shorthand): hex "#E0E0E0" gradient string CSS-like subset (see set_fill description for full grammar — same rules). Common: "linear-gradient(<angle>deg, <#hex> <pos>%, ...)", "radial-gradient(<stops>)". Rejected: "circle at X% Y%", named colors, hsl(). variable token qualified bare name "$Border/Default" To bind a variable to the stroke color, use the explicit `color` field — the shorthand parser silently drops bare-name tokens. Returns: { data: { id, name, type, applied?, noop?, rejected? } } (Batch: { data: { count, results, errors?, partial? } }). `applied`/`noop` list prop keys; `rejected: [{key, reason}]` is TERMINAL (prop invalid for this node type / readonly) — do not retry it. If the only requested prop is rejected, returns `{ error }`. `partial: true` means some batch entries succeeded and some failed — check `errors[]` for which entries to retry. |
create_vector |
params_unexplained |
Create a vector node from SVG path data or a list of points. Use for chart lines, custom icon paths, freeform curves, or any shape that needs path data. Examples: // Polyline (chart trend line) create_vector({ parent: "1:23", name: "TrendLine", x: 40, y: 20, width: 550, height: 240, points: [[0,144],[90,96],[180,120],[270,64],[360,80],[450,40],[540,72]], stroke: "#6366F1", strokeWeight: 2 }) // Raw SVG path (custom shape) create_vector({ parent: "1:23", name: "Wave", width: 200, height: 60, data: "M 0 30 Q 50 0 100 30 T 200 30", stroke: "linear-gradient(90deg, #8B5CF6 0%, #F97316 100%)", strokeWeight: 1.5 }) Path input — provide ONE of: points: [[x,y], ...] compiled to "M x0 y0 L x1 y1 ..." (polyline shortcut) data: "M ... L ..." raw SVG path (LLM-native; supports M, L, C, Q, A, Z) Stroke / fill (same formats as set_stroke / set_fill): hex "#6366F1" gradient "linear-gradient(angle, #color stop%, ...)" variable qualified bare name "$Brand/Primary" Default fill is "transparent" so the vector shows only its stroke. Pass an explicit fill if you want it filled. `windingRule` selects how overlapping/self-intersecting regions are treated when filling — use EVENODD for alternating hole patterns (e.g. ring shapes), NONZERO for the default where overlapping contours fill together. When NOT to use: - Standard rectangles / ellipses / lines — use jsx <Rect/>, <Ellipse/>, <Line/> elements (simpler, batch-friendly) - Existing vector edits — use edit / set_stroke instead |
reconcile_preview |
no_return_description |
READ-ONLY reconcile dry-run. Given an existing subtree (scope) and a target JSX (markup), computes the minimal create/update/delete/move plan that would turn the live subtree into the target — preserving the id of every matched node — and PRINTS it without touching the canvas. Use it to preview how a declarative edit would be reconciled. Returns: the computed update plan (the create/update/delete/move ops that reconcile_apply would perform). Nothing is applied to the canvas. Workflow: read_jsx({node}) to get the current subtree → edit that JSX → reconcile_preview({scope: same id, markup: your edited JSX}). |
reconcile_apply |
no_return_description |
Apply a declarative edit to an existing subtree, PRESERVING the id of every node you keep (so variable bindings, instance overrides and prototype links survive) — the id-preserving alternative to delete+rebuild. Returns: { data: { count, created, updated, moved, deleted, errors? } } summarizing the applied change plan — which nodes were created, updated in place, re-parented, removed (if allowDelete), and any per-node errors to retry. Workflow: read_jsx({node}) → edit that JSX (keep id= on kept nodes, drop id= on new ones, re-nest freely) → reconcile_apply({scope: same id, markup: edited JSX}). Matching is by explicit id only: an id= node is updated/moved in place (re-parenting = move, id kept); a node with no id is created. Props are a PATCH — only props in the markup are written, absent props are left unchanged (never cleared). Existing nodes not in the markup are KEPT unless allowDelete:true. Preview with reconcile_preview first to see the plan. |
Selection evidence
8 pairs where similar names or overlapping descriptions may send an agent toward the wrong tool.
| Tool A | Tool B | Confidence | Why they collide |
|---|---|---|---|
edit |
edit_jsx |
high | Both are generic 'edit' verbs for changing node properties. A task like 'edit the button's properties' is genuinely ambiguous: edit applies props directly on known node IDs, while edit_jsx requires reading/re-serializing canonical JSX first. An agent can plausibly pick either member and produce a different (often wrong) workflow. |
set_text |
set_fill |
medium | A task like 'make the text red' mentions text but asks for a color change; set_text changes content while set_fill (fill=text color) is correct. The shared 'text' token in the request makes mis-selection plausible, especially for LLMs that latch onto the noun 'text'. |
set_variable_value |
set_variable_mode |
medium | A task like 'set the variable to Dark mode' or 'set this element to dark' loosely mixes the concepts 'set value' vs 'set mode'. set_variable_value changes a value per mode; set_variable_mode switches a node's collection mode. Natural phrasing rarely disambiguates which is intended. |
add_component_prop |
edit_component_prop |
medium | A task like 'update the Label prop on the component' is ambiguous when the agent is unsure whether a property already exists: edit_component_prop only renames/changes defaults of an existing prop, and a wrong add call could create a duplicate or require idempotent matching. Both share the component-prop vocabulary ('component', 'prop', 'set'). |
edit |
edit_component_prop |
medium | edit's description says it handles instance component-prop overrides by display name, while edit_component_prop edits the component master's property definition. A task like 'change the Label default on the component' can plausibly target either tool, and the division (instance override vs master definition) is easy to mis-read. |
create_collection |
create_variable |
medium | A task like 'create the theme tokens' or 'set up the Theme variables' requires creating a collection AND a variable, so an agent may pick only one member. Names are both idempotent find-or-create tools described in near-identical terms, so selecting the wrong one is a plausible single-step mistake. |
bind_variable |
set_variable_mode |
medium | A task like 'make this text use the Dark mode value' could be satisfied either by binding a variable to the node (bind_variable) or by switching the node's collection mode (set_variable_mode). Both tools operate on a node + variable/collection/mode vocabulary, and the descriptions offer overlapping routes to 'show dark values'. |
get_selection |
get_screenshot |
low | A task like 'show me a preview/image of what I selected' is ambiguous: get_screenshot needs a node ID (typically obtained via get_selection), so an agent may invoke the wrong member to fulfill 'visualize my selection'. The shared 'get'-'node' framing makes the overlap plausible, though still resolveable from context. |
Compare the field