Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

The web app5 of 12

The approval card

About 6 minutes to read

On this page

When Kotoba needs a decision from you — permission to run something, or a value only you can supply — a card takes over the screen. It is the one surface in the browser that authorises anything.

Screenshot placeholder: an approval card centred over a blurred stage — a yellow tick badge, the command on one line, a "Show full command" pill under it, then a green "Yes, go ahead" and a grey "No" side by side, and a wide purple "Always allow this exact command" button below with a small grey note under it.

The kinds of card

One component draws all of them. There are three modes, and the input mode has four flavours (components/panels/InputRequestPanel.tsx:71-75, lib/store.ts:96-101):

Mode / kindWhat it asksHow the answer travels
approvalmay this action runPOST /api/session/{id}/input
input · texta value she can use out loudspoken back into the conversation, or posted if the tool is blocked waiting
input · keya reusable credential of hersposted only — masked, never echoed
input · secretyour one-time passwordposted only — masked, used once, never saved
open_linkpermission to open a URL in a new tabnothing is posted; the tab opens on your click

The card is portalled to <body> and drawn over a blurred backdrop (InputRequestPanel.tsx:43-46, 156-176).

It cannot be dismissed by accident. There is no Escape handler, and clicking the backdrop does nothing — both pinned by a test (tests/approval-card-grants.test.mjs:21-22). Every button is single-flight: a second submit from a double-click or Enter-then-click is ignored (InputRequestPanel.tsx:63, 114, 130, 140, 152).

Only the newest card is drawn, and answering it dismisses that card by its own request id — not "whichever opened last" (CompanionExperience.tsx:843-848, lib/approval-card.ts:51-53).

What an approval card shows

  • A headline. The first non-blank line of the command, capped at 120 characters (lib/approval-card.ts:1, 35-39). A card asking for a yes must name what runs.
  • Show full command. When the command is longer than the headline, the whole thing opens in a dark monospace block (InputRequestPanel.tsx:190-224).
  • A notice block, on the cards that carry one — an alert line, named fact rows, a warning, and a quoted description. Every field is drawn under a label the backend chose, so an outside server can only ever supply a value, never name a field (lib/store.ts:86-95).

Nothing in the notice is clipped. The label is one string, and clipping it would decide for you which half matters — on an external-tool install card, the half that used to lose was "Secrets needed:", on the one card where a third party is asking for a secret (InputRequestPanel.tsx:1-10).

Every drawn value is stripped of invisible and bidirectional control characters at the one door into client state (lib/store.ts:248-256). This is not decoration: a right-to-left override inside a shell label reverses the rest of the line on screen, so what you approve is not what will run. The test that pins it pushes those exact bytes through the store and checks that no readable byte and no right-to-left name is lost in the process (tests/input-request-scrub.test.mjs).

The two grants

Beside Yes, go ahead and No, a card may offer up to two standing grants (lib/approval-card.ts:28-33). Which appear is the backend's decision, sent as flags on the card, and the browser never second-guesses them.

Always allow this exact command

Saves this one command line, character for character. The stored key is the text you read on the card, and matching is byte-for-byte: no prefix, no glob, no whitespace folding, no case folding (api/src/kotoba/core/approval.py:458-473). echo a and echo a are two different grants, and that is the point — you granted the line you read.

The card says so, in one sentence under the button: "Only this line, exactly as written — anything else still asks. Revoke it in Settings." (InputRequestPanel.tsx:86-87.)

It is withheld in three cases (api/src/kotoba/core/approval.py:350-368): a command matching a dangerous pattern, an action whose family was named explicitly by the tool rather than derived from the command line, and Windows — where no stored grant is consulted at all.

Always allow "npm" — don't ask again

The broader grant. The button carries the family name, or reads Always allow her to run Python when the action is code rather than a shell command (InputRequestPanel.tsx:80-85).

It saves a command family — the first token of the command line — so every future command starting with that token runs unasked. The note under it says exactly that: "Every "npm" command runs without asking, until you revoke it in Settings." For code execution it reads differently and more bluntly: "Any code she writes then runs on this computer without asking, until you revoke it in Settings." (InputRequestPanel.tsx:80-92.)

A family is a summary of an infinite set of future commands, and the summary is lossy. So it is withheld whenever the summary would stop naming what runs (api/src/kotoba/core/approval.py:324-347):

  • the command matches a dangerous pattern;
  • the line chains commandsnpm run build && ./deploy.sh is not described by npm, and a saved npm rule would never match that line again while silently granting every simple npm;
  • the family is an interpreter or an exec wrapper — saving sh, python or env is saving arbitrary execution;
  • the host is Windows.

When the family grant is withheld, the card says why. One sentence, generated by the backend so the browser and the terminal cannot disagree (api/src/kotoba/core/approval.py:371-397), drawn under the buttons (InputRequestPanel.tsx:292-296):

This line chains commands, so a rule for "npm" would never match it again. "sh" only names what runs it, not what runs — saving that would allow anything. It's a recursive delete, so it asks every time — this one can't be saved.

A chained command still offers the narrow grant. Chaining removes the family button, not the exact one: the exact key is the whole line as one string, so metacharacters inside it are part of that one string rather than an escape from a shorter one (api/src/kotoba/core/approval.py:351-357).

Both grants are revocable from Settings → Security → Allowed commands, and each row says which width it is: just this line: … or any "npm" command (SettingsPanel.tsx:482-490).

Cards that ask you to type

The field is masked for a key or a secret, with a reveal control. A line under the box states which of the two it is (InputRequestPanel.tsx:336-342):

  • secret"Used once for this step and then discarded — it's never saved and never appears in the conversation."
  • key"Saved with your credentials on the server only — it never appears in the conversation."

A blocking card — any key or secret, or any card the backend marked as waiting — delivers its value by POST and the value never re-enters the conversation (lib/approval-card.ts:41-49, InputRequestPanel.tsx:138-147). A non-blocking text card is spoken back into the turn instead, because that is what it is for. The call shell enforces the same rule from its side: a posted value is never echoed into the transcript (CompanionExperience.tsx:600-608).

A key card also sends its storage name, or the backend cannot file it and the value would be promised and then dropped (lib/approval-card.ts:41-49).

The URL is shown in full and is also a real anchor. Clicking either the anchor or Open in new tab opens it with noopener,noreferrer, on your gesture so it is not blocked as a popup. Nothing is posted back and nothing is echoed (InputRequestPanel.tsx:112-118, 298-317). Not now just closes the card.

While the events channel is down

Approval cards ride the events channel. When that channel drops, the stage notice says so and names the consequence: "Approvals and live progress are paused until it's back." (lib/notices.ts:14-16, 79.) Nothing is silently approved; the card simply does not arrive until the channel is back.