The command layer as a design
oniyanma has no "API for AI". The AI uses exactly what the UI uses.
Five entry points go through one layer
UI buttons ─┐
⌘K palette ─┤
Shortcuts ─┼─→ executeCommand(ctx, name, args, actor) ─→ Viewer / Nav / Measure …
AI console ─┤
MCP / relay ─┘Adding a new entry point doesn't change where it goes through. This isn't an implementation convenience — it's how the following four things get decided in one place.
Commands declare their own properties
Alongside its name, description, and argument JSON Schema, each command carries whether it's read-only (readonly) and whether it's destructive (dryRun) itself.
{
name: 'queryElevation',
description: 'Get elevation (real Z, m) distribution: {min,max,mean,count,bins[]}…',
input_schema: { type: 'object', properties: { bins: { type: 'number', minimum: 2, maximum: 128 } } },
readonly: true,
run: (c, a) => c.viewer.queryElevation(…),
}This marker alone decides the following four things at once.
1. Tool definitions. toolDefs() generates them directly from the registry. It's Anthropic tool-use compatible, so there's nowhere to hand-write the definitions passed to the AI. There's structurally no room for the definition to drift from the implementation.
2. Permissions. Instead of a separate per-command ACL table, permissions are decided by the surface — which key you came in through.
3. The confirmation gate. When a machine calls a command that has dryRun without confirm, it doesn't execute — only the affected count comes back. → Confirmation gate
4. Documentation. The argument tables in the command list are generated from that same definition when this documentation is built.
Adding one command and marking its properties is enough for the public surface, denials, tests, and documentation to follow automatically.
Surfaces
The read/write split isn't per command — it's per entry point.
| Surface | What can run |
|---|---|
all (default) | All 59 commands. The UI / ⌘K / AI console use this |
read | Only the 18 readonly commands |
Building a read-only entry point means write commands can't execute from it. Just trimming the tool definitions would leave "not listed, but goes through if you know the name", so it's also blocked at execution time. The refusal message includes the list of commands that are available, so the AI can understand "no permission" and switch to an alternative (answer from a read, or ask a human).
Restricting the entry point handed to a client, surveyor, or consultant to this surface shrinks the permission design from "build an ACL per command" down to "which surface is open". A read-only surface's results can be cached too, and exposing MCP externally with a read-only token can't break anything.
Actor
Where a surface is "which key you came in through", the actor is "who". It's injected on every dispatch, and edit commands bake it into the Command log.
kind | Source | Confirmation gate |
|---|---|---|
human | UI / ⌘K / shortcuts | Doesn't apply (you're looking at your own result) |
ai | The AI console (the model name and the start of the instruction are also recorded) | Applies |
api | Via window.oniyanma / MCP | Applies |
This lets "what the AI touched" be separated out later, making the deliverable auditable.
Tests protect five directions at once
Tests for the command layer (argument validation, dispatch, tool-definition validity, actor injection) protect all five entry points at the same time. There's no need to write tests per entry point.
On top of that sits the eval set, which measures the conversion from natural language to a command sequence.