01Safety
0.0 / 30
What changed in the harness
Selection accuracy 98→100, token cost down 5%, 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
23.9 / 30
7.1 / 20
11.2 / 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
10 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 |
|---|---|---|
jsx |
no_return_description |
Create design trees with nested JSX markup. One jsx call builds a complete subtree atomically — nesting is the hierarchy. Keep a single logical unit inside one call; the returned root's children are already built, not stubs to be filled in later. Supports elements frame, text, rect, ellipse, line, icon, image, instance, component, group, section, vector, with layout/fill/effect/typography/grid attributes and rich-text markup as documented in examples. Pass `parent` to append under an existing node, or `replaceId` to atomically swap an existing subtree at the same parent and sibling index (markup must have a single root). Returns the created or replaced root node's id, name, and type, along with its fully built child subtree. |
move_node |
no_return_description |
Relocate a node 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, (d) renaming a node in place via `name`. Returns the node's id, name, and its resulting parent and sibling index after the move/rename. 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. Returns the new clone's id, name, and parent so it can be referenced immediately without a follow-up lookup. 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 on it and its descendants show dark values. Returns { data: { ok: true } } confirming the node's mode selection for that collection was updated. 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 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; `variantOptions` (list of option strings) is only meaningful on that redirected VARIANT path and is otherwise ignored. 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. 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". 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, per node: { data: { id, name, type, applied?, noop?, rejected? } } (single) or { data: { count, results, errors?, partial? } } (batch) — `applied` lists the fill/bg keys written, `noop` lists keys already at that value, `rejected` lists keys the node type can't accept. |
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, per node: { data: { id, name, type, applied?, noop?, rejected? } } (single) or { data: { count, results, errors?, partial? } } (batch), listing which stroke properties were applied, already matched (noop), or rejected as invalid for the node type. |
create_vector |
params_unexplained no_return_description |
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) `windingRule` sets the SVG fill-rule for self-intersecting or overlapping path regions: "NONZERO" (default, fills based on path direction) or "EVENODD" (fills based on crossing count) — only matters when the path is filled (not just stroked) and self-overlaps. 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. Returns the created vector node's id, name, and type. 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 returns that plan as a list of per-node operations (create/update/delete/move, with the affected node id and the props/position that would change) without touching the canvas. Use it to preview how a declarative edit would be reconciled. Workflow: read_jsx({node}) to get the current subtree → edit that JSX → reconcile_preview({scope: same id, markup: your edited JSX}). Nothing is applied. |
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. 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. Returns a summary of the applied changes — which node ids were created, updated, moved, and (if allowDelete was true) deleted. |
Selection evidence
17 pairs where similar names or overlapping descriptions may send an agent toward the wrong tool.
| Tool A | Tool B | Confidence | Why they collide |
|---|---|---|---|
jsx |
edit_jsx |
medium | Both operate on JSX text for a subtree; a task like 'update this button's rounded corners' could plausibly be attempted with jsx's replaceId instead of the intended read_jsx+edit_jsx patch flow, though jsx is scoped to creation/replace and edit_jsx to in-place property patches. |
jsx |
read_jsx |
low | Same JSX dialect and naming, but one creates/replaces trees and the other only reads — a request to 'see the JSX for this frame' is unlikely to be misrouted to the write tool. |
read_jsx |
edit_jsx |
low | They're a mandated read-then-write pair by design (read_jsx output feeds edit_jsx), so an agent might call read_jsx alone when asked to 'edit' something, but the tools' purposes are clearly distinct (read vs write). |
edit |
edit_jsx |
medium | Both are generic property-update tools named 'edit*'; a task like 'change this node's corner radius and background' could be routed to either edit (direct prop patch) or edit_jsx (text-based patch via read_jsx), especially since edit's description explicitly contrasts itself with edit_jsx for tree-level edits. |
add_component_prop |
edit_component_prop |
medium | A task like 'update the Label property's default value' could be misrouted to add_component_prop instead of edit_component_prop if the agent doesn't realize the prop already exists, since both configure component properties with overlapping parameter names (name, default). |
add_component_prop |
delete_component_prop |
low | Opposite operations (add vs remove) on the same prop concept are unlikely to be confused given clear verb semantics, despite shared component/prop vocabulary. |
add_component_prop |
list_component_props |
low | list is read-only discovery vs add is a mutation; a vague 'what properties does this have' vs 'add a property' are distinguishable asks, low ambiguity despite shared tokens. |
edit_component_prop |
delete_component_prop |
low | Rename/default-change vs full removal are semantically distinct verbs, unlikely to be confused despite operating on the same target type. |
list_component_props |
delete_component_prop |
low | One lists, one deletes — different verbs make confusion unlikely despite shared component/prop tokens. |
list_component_props |
edit_component_prop |
low | Discovery vs mutation are clearly different intents; low risk of an agent picking the wrong one. |
set_variable_value |
set_variable_mode |
medium | Both take a variable/node + mode + 'set' semantics; a task like 'set this to dark mode' could be misrouted to set_variable_value (which sets the value FOR a mode) instead of set_variable_mode (which sets WHICH mode a node uses), since the phrasing 'set mode' is ambiguous between the two tools. |
create_collection |
create_variable |
medium | A request like 'create a Theme variable with Light/Dark values' could be ambiguous about whether to create the collection or the variable first, and an agent might call create_variable expecting it to also create the collection since both are idempotent find-or-create with overlapping 'collection'/'variable' naming. |
delete_collection |
delete_variable |
medium | A task like 'delete the Theme colors' could target either the whole collection or a single variable within it; both cascade-unbind similarly, so the agent must correctly infer scope from user phrasing. |
bind_variable |
set_variable_mode |
medium | Both take node+variable/collection+mode-like args and relate to making a node reflect a variable; a task like 'make this node use the Dark theme variable' could be misrouted to bind_variable when set_variable_mode is actually needed to switch which mode a node displays. |
set_fill |
set_stroke |
low | Both set node color properties with similar signatures, but 'fill/background' vs 'border/stroke' are distinct enough in common usage that confusion is unlikely for a clear task. |
discover_props |
replace_props |
low | One is read-only audit, the other is destructive bulk-write; distinguishable by intent (survey vs replace) despite shared subtree/prop vocabulary. |
create_component |
combine_components |
low | create_component converts a single frame; combine_components merges multiple components into a variant set — different cardinality and purpose make confusion unlikely despite shared 'component' token. |
Compare the field