itda
Just type an address after itda and your AI can use the site.
Give itda one address. It learns what the site can do and tries it. It gives your AI agent a short list of actions in plain words. Reads just work. Nothing changes on the site until you say yes. It runs on your own computer, and it works on the old intranet behind a login too.
It is for the sites your company runs and the services you have an account on, not the web at large. What it can and cannot do lists both, and what happens when a site changes.
$ itda news.ycombinator.com
1/5 Looking at news.ycombinator.com...
The page opens without signing in.
Found: a list of 30 records, a form, a feed.
2/5 What agents will be able to do:
read home List what Hacker News shows (inputs: p)
read feed Read the RSS feed
3/5 Trying every read once...
✓ home 30 records
✓ feed ok
4/5 Saved as 'news-ycombinator'.
Connect Claude Desktop? [Y/n]
5/5 Adding itda to Claude Desktop...
Ask your agent: “use news-ycombinator to list what Hacker News shows”. A site behind a sign-in opens in your own browser. You sign in and use it as usual while itda watches. What you type never leaves your computer.
Why
Agents that drive a browser re-read every page and spend thousands of tokens on each screenshot. They follow whatever the page tells them and break silently when a layout changes. itda moves that work to a setup step. A site definition says which actions exist, what they send, what a correct answer looks like and whether they change anything. After that, every call is small, typed and checked.
- Checked. A signed-out page or a redesigned site comes back as an error with what to do next, never as data.
- Read-only by default. Changes wait for a person to approve the exact request, in a terminal or in the agent app. No agent tool can approve.
- Adaptive. Reads can recover through browser fetch and rendering. Unresolved reads fail without locking the site; changes are never replayed.
- Accountable. A local log records every call, approval and change. Each entry seals the one before it, so edits show.
- Local. No server, account or telemetry. Secrets stay in your OS keyring.
Where to go next
- Quick start: install, set up a site you use, and try the bundled sandbox.
- Use it from an agent: connect Claude, Cursor, VS Code or Codex.
- Writing a site definition: add your own site by hand.
Quick start
Install
macOS and Linux:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/brnyxx/itda-cli/releases/latest/download/itda-cli-installer.sh | sh
Windows (PowerShell):
powershell -ExecutionPolicy Bypass -c "irm https://github.com/brnyxx/itda-cli/releases/latest/download/itda-cli-installer.ps1 | iex"
Download an archive from
Releases, or build from source
with cargo install --git https://github.com/brnyxx/itda-cli itda-cli.
Claude Desktop, with no terminal: download the bundle for
macOS (Apple silicon),
macOS (Intel) or
Windows and open the file. Claude
Desktop installs itda with its tools, and “use the-site-you-use.example” sets
a site up from inside the conversation. Approving a change still happens in a
terminal with itda approve, so for changes install the command too.
Check the setup:
$ itda doctor
itda 0.7.0
ok data directory /home/you/.local/share/itda
ok secrets system keyring; long secrets sealed in ~/.local/share/itda/vault.sealed with a key from the keyring
ok audit log 0 records, chain intact
ok changes none with an unknown outcome
ok sites 0 installed, 0 invalid
ok agents none connected (itda setup <url>, or itda mcp install <client>)
ok local addresses refused (ITDA_ALLOW_LOCAL=1 allows them)
Set up a site you use
Type itda and paste the address of a site you use. Press Enter. itda looks at
the page. When the page is drawn by scripts and shows itda only a title, it
opens your browser on it for a few seconds. It watches what the page loads by
itself and drafts from those calls. Nothing is clicked or typed, and the window
closes on its own. If the page needs a sign-in, it asks for your user name and
password and fills in the site’s own form for you (--user <name> from a script,
the password on stdin). Or it asks to open your browser and watch you use the
site. It shows what agents can do and tries every read. After one more Enter, it
connects the agent apps it finds on your computer. From then on, ask your agent
for things on that site. itda setup <address> does the same from a script.
--no-record never opens the browser; --watch-only watches but never asks you
to record. itda setup --help lists the flags. itda speaks the language of
your LANG; ITDA_LANG=ja (or any tag in docs/i18n.md) overrides it.
itda recognises sites running known software (GitLab, Jira, Confluence, Redmine,
Discourse, MediaWiki, WordPress, Gitea or Forgejo) from their pages and confirms
each with one request to the software’s own API. itda then says “This looks like
GitLab. Using its API at --as <software> forces a template when the page does not say;
--no-template skips them.
From inside your agent
Once itda is connected (itda mcp install <client>, or the question at the end
of a setup), you can skip the terminal for the next sites. Tell your agent “use
news.ycombinator.com”. It calls itda_setup with the address. itda looks at the
site, drafts and tries it the same way. Its actions appear as tools in the same
conversation. A page drawn by scripts opens briefly in a browser window on your
screen. itda watches what it loads without clicking or typing. When a read says
to sign in, the agent calls itda_login. Your own browser opens for you to sign
in; the agent only sees the account id. itda_check, itda_sites and
itda_manage cover checking, listing, removing and resuming sites and reading
approvals and the audit log, with a form for you to confirm the ones that change
something. Nothing an agent can call approves a change.
Try the sandbox
The repository includes a small fake intranet that only listens on 127.0.0.1: a notice board with an HTML table, a JSON API behind an API key, and a page that refuses automated access.
git clone https://github.com/brnyxx/itda-cli && cd itda-cli
cargo run -p itda-sandbox # http://127.0.0.1:8787
In another terminal, use a scratch data directory and install its definition:
export ITDA_HOME=$(mktemp -d) ITDA_ALLOW_LOCAL=1
mkdir -p "$ITDA_HOME/specs/sandbox"
cp examples/sandbox/spec.yaml "$ITDA_HOME/specs/sandbox/"
itda ls
Read, sign in, and see what happens when a site says no:
itda call sandbox.list-notices --q 보안 # JSON API
itda call sandbox.notice-board --json # HTML table extracted into records
itda call sandbox.me # exit 10: no API key yet
echo sandbox-key | itda login sandbox # stores the key in the OS keyring
itda call sandbox.me # signed in
itda call sandbox.reports # exit 12: refused, in your browser too; nothing is paused
Make a change:
itda call sandbox.create-notice --title hi # exit 20: waits for approval <id>
itda approve <id> # shows what it does and the exact request, asks y/N
itda call sandbox.create-notice --title hi --approval <id>
itda audit # everything above, hash-chained
itda describe sandbox # every action with its inputs and a command to copy
Without a keyring (CI, containers), skip itda login and set
ITDA_SECRET_STORE=env ITDA_SECRET__SANDBOX__DEFAULT__API_KEY=sandbox-key.
Requests to one site go one at a time, about 1.2 seconds apart, as on a real
website. Consecutive or parallel calls take a moment. A site definition can ask
for more room with defaults.interval.
Commands
| Command | What it does |
|---|---|
itda setup <url> [--name id] [--record|--no-record] [--as software|--no-template] [--client name|none] [--yes] | In one go: look at the page or record you using the site. Name the actions in plain words (in your language: ITDA_LANG or LANG). Try every read and keep what works. Connect an agent. Known software (GitLab, Jira, Confluence, Redmine, Discourse, MediaWiki, WordPress, Gitea) gets its API from a template. --as forces one; --no-template skips them |
itda draft page <url>… [--name id] [--base-url url] [--include text] [--install] [--force] | Draft a site definition from the pages you use: tables, lists, embedded data, forms and feeds, plus the reads of the API description the site publishes |
itda record <url> [--name id] [--install] [--save file.har] | Use a site in your own browser while itda records what its pages ask for. Draft a definition from the recording |
itda draft openapi <file> [--install] · itda draft har <file> [--install] | Draft one from an API description or a saved browser recording |
itda add <file|url|name|-> [--force] | Install a site definition (a file, an https URL, stdin, or a ready-made site such as github) after showing where it sends requests and which actions change the site |
itda spec validate <file> [--json] | Check a definition the way itda add would, without installing it. Exit 0 when it is valid; otherwise every problem and exit 2. --json lists the issues |
itda remove <site> | Remove an installed site definition; its accounts stay until itda logout |
itda ls [site] [--json] | Installed sites and their actions |
itda describe <site>[.<action>] [--json] | Each action with its inputs and an example command to copy; for a change, the read that confirms it after a lost answer |
itda call <site>.<action> [--input value]… [--account id] [--approval id] [--json] | Run one action |
itda login <site> [--user name] [--account id] | Sign in through the site’s own form, named in the definition. itda takes the password from a prompt or stdin and keeps the session in the keyring. |
itda login <site> --secret <name> [--account id] | Read a secret, such as an API key, from stdin and store it under that name |
itda login <site> --browser [--url page] | Sign in in your own browser and keep that site’s session |
itda accounts [<site>] [--json] | Signed-in accounts and their secret names, never the values |
itda logout <site> [--account id] | Remove an account and its saved secrets |
itda resume <site> | Resume after a block or an expired sign-in |
itda approvals · approve <id> · reject <id> | Review changes agents asked for; reject also withdraws one you granted |
itda audit [--limit n] [--verify] [--json] | What agents did, and the changes a stopped process left without an outcome |
itda mcp [--mode direct|catalog] [--approve-in-client] [--site name] | Serve installed sites over MCP, with tools to set up, sign in, check and manage sites from the agent; --site serves one site only |
itda mcp install <client> [--dry-run] | Add itda to an MCP client |
itda doctor | Check the setup |
itda feedback [--no-open] | Print and open a link to a new GitHub issue with the version, the platform and the doctor checks filled in; sends nothing itself |
itda check [<site>|--all] [--account id] [--json] | Run every read that needs no input. End each site with a verdict and what to do: ok, signed_out, site_changed, blocked or unreachable. --all prints one verdict line per site. Run it when an action that used to work fails, before retrying or re-drafting |
itda refresh <site> [-y] | When a check says site_changed, draft the site again from its original sources. Show actions added, removed or changed, and write after a yes. Keep the sign-in, defaults, name and every action you edited by hand. Checks the site |
Use it from an agent
The repository has two files for the agent itself: AGENTS.md, a guide to using itda well, and skills/itda/SKILL.md, the same guide in the short form skill loaders take. The MCP server hands the agent the same rules as its instructions.
MCP clients
itda mcp install claude-desktop # or cursor, vscode, codex
itda mcp install claude-code # prints the `claude mcp add` command
| Client | File itda changes |
|---|---|
claude-desktop | claude_desktop_config.json in the Claude app’s folder |
cursor | ~/.cursor/mcp.json |
vscode | .vscode/mcp.json in the current folder |
codex | ~/.codex/config.toml |
claude-code | none; prints claude mcp add --scope user itda -- … |
The installer keeps your other servers and settings and saves the previous file
as .bak. It changes nothing if itda is already there. --dry-run shows the
new file without writing it. It starts itda by its absolute path and passes
ITDA_HOME, ITDA_ALLOW_LOCAL and ITDA_SECRET_STORE along when they are set.
Restart the client afterwards.
What the agent sees
With up to 30 actions installed, each is a tool named like the CLI
(sandbox.list-notices), with a JSON Schema for inputs. Hints: reads are
readOnlyHint; DELETE actions are destructiveHint; PUT and DELETE are
idempotentHint.
With more actions, itda lists three fixed tools. The tool list stays short and prompt caches stay valid:
| Tool | Use |
|---|---|
itda_search | Find actions by keywords |
itda_describe | Show one action’s input schema and effect |
itda_call | Run an action with args (and approval for a change, next for the next page of a list) |
itda mcp --mode direct or --mode catalog overrides the choice.
itda mcp --site <name> serves only that site’s actions as tools. It refuses
itda_setup and itda_prepare and confines itda_login, itda_check, itda_sites,
itda_manage and itda_shape to that site. Use it in a project’s MCP configuration when the
agent should only reach one site from there.
Seven tools are always there, so the agent never needs a terminal to set a site up or look after it:
| Tool | Use |
|---|---|
itda_setup | Set up a site from a page address: look at the page as itda and draft. Try every read once and keep what works. The site’s actions become tools in the same session. The server declares tools.listChanged and sends notifications/tools/list_changed; the next tools/list has them. A script-drawn page opens briefly in a browser window on your screen. itda watches it load on its own, without anyone clicking or typing. itda refuses pages that need a sign-in or still show nothing and gives you the command to run. It never asks you to record here. |
itda_login | Open your own browser to sign in, exactly like itda login <site> --browser. itda seals the session under the data directory with a key from the keyring; the agent only sees the account id. A client with forms asks you first. Nobody presses Enter, so the definition must name the cookie that shows a sign-in. |
itda_check | itda check for one site, or every site when none is named: the verdict per site and what to do, as itda check --json prints them. |
itda_sites | What is set up, with reads, changes, the last check’s time and verdict, how many actions answered on their last try (verified), the recent pass_rate and the reads that fail now and then (unstable). With site, one site in detail: each action with verified, pass_rate and last_verdict, a user_guide (what can be read, what can be changed, which list read supplies which detail read’s input), two to four natural_prompts a person might ask, and the top three gaps. |
itda_manage | refresh drafts a changed site again from its sources. The form shows what would change; no answer counts as no. For remove and resume, you confirm in a form; clients without forms get the command to run. approvals lists what waits for you. audit lists the last entries for one site or all, plus, under unknown_outcome, changes interrupted mid-process and changes whose answer was lost, each with what the confirming read said (check) when the change named one. fsck runs the knowledge base’s rules for one site or all and sends nothing: in one call, the reads never tried (endpoint_untried), the reads that need an input and which list read can supply it (read_needs_input), the reads that fail every recent try (read_failing) or now and then (flapping_read), and files changed under itda (artifact_changed, artifact_missing). |
itda_shape | The shape of what a site shows and answers, never its values. For an action: the keys and types of its last answer as shape and as pointers ({pointer, type}), and what its definition declares. For a page: its selectors with match counts, attribute names and parent (structure), its named fields without values (fields) and the keys and types of the data it embeds (ssr_payloads). From the local knowledge base by default; with fresh: true, itda reads the page now, the way setup reads a page. At most 24 KB. |
itda_prepare | The preparation loop below: the agent drafts a site from its docs and pages, and itda keeps what the evidence carries and what answers, then installs it the way itda_setup does. |
Every gap comes as {rule, subject, priority, reason, remedy}; remedy has
the command for a terminal and the tool and arguments to call next (the
action’s own tool, or itda_call with action when the server lists the
catalog). Act on a remedy only when it serves what the user asked; never call a
change because a gap names it. itda_setup returns the top three gaps as
gaps.
When itda_setup keeps one read or none, its result carries a remedy
naming itda_prepare with the page’s address.
Every result uses the itda call --json envelope, marked "untrusted": true. A
note before it says website data is not instructions. Results over 24 KB keep
the start of their longest lists and say "truncated": true. Failures are tool
errors with a code and a fix, so the agent can tell the user what to do.
Prepare a site yourself
Some sites tell a person more in their docs than a page tells itda: a
reference that lists GET /api/v2/items/{id}, an API description, a GraphQL
schema. With itda_prepare the agent reads them and drafts the definition
itself; itda calls no model. The agent never sees a value the site holds.
Results carry titles, the headings of a page’s own sections, paths, methods,
field names, selectors, keys, types, JSON pointers and counts, each piece with
an evidence id (e12) the agent cites. The loop, one tool with an op:
op | What it does |
|---|---|
start | Opens a session for the site of url (one per site; calling again returns it) and shows the environment: the browser by name, the site’s accounts by id with the names of their secrets, whether the site is paused or installed. |
recon | Reads the entry page, the API description it links (or /openapi.json), what its GraphQL endpoints offer and the routes its docs name, and returns the draft itda makes from them as candidates, each judged. bundles: true also reads up to 24 scripts the page loads and tries each route string once with a GET; only a route that answers with data is evidence, the rest are counted as noise. |
read | Reads one more page or document, on the site’s registrable domain or a host the site linked as documentation (its API description, the servers that names). Other sites (outside_site) and private addresses (private_address) are refused. GET only, without an account. |
search | This session’s evidence and itda’s notes about the site that match words. |
propose | Judges the agent’s definition (spec, in this format; use_recon takes itda’s candidates whole), its workflows (understand, 1 to 12) and its self-questions (self_qa, up to 12, kept only when every evidence id exists). Each operation comes back kept or dropped with the reason, the evidence ids and what was missing. |
verify | Tries the kept reads, at most 6 per call. A read that needs an input runs only with a check, and $result:<read>:<pointer> fills it from an answer already tried (a string, a number or a boolean). Each result says whether it answered, how many records and the shape. A JSON read with no required_pointers gets them from the field names at the top of the answer (none for a list or an object keyed by data). A page read that fails says in diagnosis, in counts, where its selectors miss on the page as read before. |
review | Which kept operations cover each step of each workflow, and which routes the docs or the description name that nothing implements. |
session | inspect the site’s accounts and credential sources as shapes; verify one signed-in read as an account; read one path with the account’s session, after the site is installed and the user signed in. |
install | The same path as itda_setup: save, try every read once, keep what works, add the site’s tools. replace: true only when the user asked. |
An operation is kept only when its base_url is a server the site documented
or answered from, its method and path are a route in the docs, a description
or a script that answered, or, for a GET, a path itda read (/users/@me
matches /api/v10/users/@me), an HTML read has an html extraction (records
or page fields; with required_html_fields alone it has nothing to return), a file read declares format: file, and a signed-in operation
has a path read with the session. A declared read whose request asks for a
change is a change. A route the description says needs a sign-in itda cannot
use is dropped. A draft that only reads the home page and checks nothing is
refused (no_actions). Changes are written into the definition and never
sent; the user approves each later, as for any site.
A session has 120 tool calls, 48 requests for pages and documents, 24
requests for checks (browser recovery counted, from the sites’ own pacing
record), and two rounds each for coverage and repairs. Past them,
budget_exhausted or round_limit says to install what is kept or stop.
Every result has feedback entries {kind, subject, reason, next}; next
is the call to make. A call with the same arguments is answered from the
session, not sent again. A site that refuses itda (blocked) means pause
and tell the person. The checks are tries of the knowledge base’s prepare
stage, which itda fsck and the pass rates leave out.
skills/itda-prepare/SKILL.md
is the loop in the short form skill loaders take.
Files
A read declared format: file saves its answer on the user’s computer and
returns a manifest in data, never the bytes: {path, bytes, media_type, sha256}. The file lands under files/<site>/ in the data directory, or where
itda call ... --out <dir> says; the command prints Saved <path> (<bytes> bytes, sha256 …) once. Over MCP the result carries the manifest and a
resource_link to the file, and the tool’s description says so. A failed
download says why in one word, and keeps nothing: wrong_media_type (the site
answered with a page or another kind of content), partial_answer (a 206 or a
short body), empty_answer, too_large. itda check skips file reads
(“saves a file”). itda setup tries each one once by its headers and first
bytes (the media type and size the definition allows) and keeps nothing, so a
site whose only reads are files is set up like any other.
A change whose body carries a file takes the path of a file on the user’s
computer in the input its definition names. The user approves that exact file:
the review shows its name, size and SHA-256, and other bytes under the same
path withdraw the approval (approval_expired, reason: file_changed). A
file that changes while it is being sent ends the call with
action_outcome_unknown and reason: file_changed; as with any lost answer,
never call it again on your own.
Lists that go on
A read with pagination in its definition returns meta.next if the site
reports another page. It is an opaque handle such as
ic_4f0a4207d04f4a0fb4e059d26bd54b6d. The agent passes it back as next
(--next on the command line) with the same inputs to get the following page.
That page may have its own meta.next. The site’s own marker, a cursor, page
number or offset, stays on the user’s computer. itda stores it in the local
database for fifteen minutes, bound to that site, action, account, inputs and
definition. itda removes it from the data it appears in. itda refuses handles
for another action, account or inputs, or after a definition change or expiry,
with pagination_invalid or pagination_expired (exit 23). The agent starts
again without next. Only reads turn pages. itda sends a change once. The
server instructions, AGENTS.md and the skill tell the agent to read only as
many pages as the question needs.
Changes
A change never runs on the first call. The agent gets approval_required, an
approval id and instructions to ask you. You run itda approve <id> in a
terminal. You see what the action does, the exact request and every input, and
answer y. The agent calls again with the same inputs and the approval id;
itda sends the change once. Until you answer, that call returns
approval_pending (exit 20) and the approval stays. A call with other inputs
withdraws it (approval_expired with reason: inputs_changed). With
--approve-in-client, clients that show forms ask you directly instead. See
Approvals and the audit log.
itda sends a change once. If a request gets no reply after sending (a timeout, a
dropped connection), or a GraphQL reply carries errors behind a 2xx, the call
fails with action_outcome_unknown (exit 22). The audit log records this
outcome: the site may or may not have applied the change. itda never resends a
change on its own. An agent must not either. When the change names the read
that shows it (confirm in its definition), itda runs that read once and puts
the result next to the error as outcome_check: landed means the change is
in, so tell the user and do not ask for it again; not_found means the site
shows no trace of it, so the user may ask for a new approval; unknown means
that read failed too. Without outcome_check, check the site first; only a
person decides whether to ask for the change again. Which read a change names
shows before you call it: itda describe prints it under the change (after a lost answer: checks list-notices for the new title), --json carries it as
confirm: {read, match}, and the change’s tool description (or
itda_describe in catalog mode) says the same. A new approval request for
a change that landed carries previous_outcome: landed, and its review tells
the user so. A change that never left the
machine (no turn at the gate, a name that did not resolve, a connection that was
refused) keeps its network code. itda reports it as not sent. Agents may retry
reads, which keep their usual codes (timeout, request_failed).
A 403 that is not a sign-in problem is api_denied (exit 14): the site refused
that one request as not allowed. The error carries upstream_status. It
includes provider_reason only if the site’s JSON answer names access_denied,
permission_denied, insufficient_scope, insufficient_permissions,
service_disabled, forbidden or not_authorized. itda passes on nothing else
from the answer. A 403 whose reason says the site rejected the session itself
(unauthenticated, invalid_token, token_expired, session_expired,
login_required) stays auth_required. Every refusal carries
upstream_status.
When something that worked stops working
Run itda check <site> before retrying or re-drafting (itda check --all
checks every site and prints one verdict line each). It runs every read needing
no input and never sends a change. Each site ends with one verdict and what to
do:
| Verdict | Meaning | What to do |
|---|---|---|
ok | every read still matches | retry the action; the failure was not the site |
signed_out | the site no longer accepts the account | ask the user to run itda login <site> |
site_changed | a page or answer no longer matches its definition, the address is gone (404, 405, 410) or the site no longer accepts the query | Ask the user to run itda refresh <site>, or call itda_manage with refresh. itda drafts the site again from its original sources. The user confirms the diff; itda keeps the sign-in, name and hand-edited actions. |
blocked | read recovery failed or the site rate limited itda | respect retry_after; paused: false needs no resume. Only an explicitly paused site needs itda resume <site> |
unreachable | the site did not answer | wait, then run itda check again |
A site with several problems gets one verdict, in this order: signed_out, then
site_changed, blocked, unreachable. The exit code follows the worst
verdict across the sites checked: 10, 11, 12 or 30 as in the table below, or 0
for ok. A read the site refused (api_denied) does not affect the site’s
overall verdict. The verdict uses the other reads; the hint names the refused
read. --json adds verdict, one entry per site under sites, and every
action under results with the HTTP status the site answered as
upstream_status.
Shell agents
Agents that only have a shell can call itda call <site>.<action> --json and
branch on the exit code:
| Exit | Meaning | What to do |
|---|---|---|
| 0 | ok | |
| 2 | bad input or unknown action (the remedy names the nearest one), a read that preflight would have turned into a change (effect_changed), or a read whose built request itda cannot classify (effect_unknown, the remedy says what is missing) | itda describe <site> |
| 10 | signed out or no key | ask the user to run itda login |
| 11 | the page no longer matches its definition, the site answered the query with errors (query_failed), or a file read got a page, a part or nothing (wrong_media_type, partial_answer, empty_answer) | update the definition, or check the inputs |
| 12 | the site refused this read, in the browser too (blocked_by_site) | tell the user to check the site in their browser. With paused: false other reads still work and nothing needs resuming; only a site paused in direct-HTTP mode (ITDA_TRANSPORT=http) needs itda resume |
| 13 | rate limited | wait as long as retry_after says, then retry the read |
| 14 | the site refused this one request as not allowed (api_denied) | tell the user what provider_reason says; do not sign in again |
| 20 | needs approval (approval_required), the user has not answered yet (approval_pending), or changes are off | ask the user to run itda approve <id>, or wait and call again with the same id |
| 21 | approval used, rejected, expired or withdrawn because the inputs changed | call again for a new one |
| 22 | itda sent a change; its outcome is unknown | check the site before retrying |
| 23 | the next handle expired, or was for another call | call again without --next |
| 30 | network error, an answer over the read’s size cap (too_large), or a turn lost during a long transfer (request_lease_lost) | retry later |
With --json, itda prints errors on stdout as
{"status": "<code>", "message": …, "remedy": …} plus details such as
approval.
Approvals and the audit log
How a change is approved
-
An agent calls an action that changes something. itda records the exact request and answers
approval_requiredwith an id. Nothing is sent. -
You list what is waiting with
itda approvals, then runitda approve <id>in a terminal:$ itda approve 9e96f90486f74733926ffaeb738825e2 create-notice wants to change Sandbox intranet (sandbox) as account default: does Post a notice request POST http://127.0.0.1:8787/api/notices title 점검 공지 Approve this change? [y/N] y Approved. The agent can now run it once, with exactly these inputs.doesis the action’s description from the site definition, followed by every input. itda cuts values longer than 500 characters and shows their length.itda reject <id>, or any answer buty, refuses it.itda rejectalso withdraws an approval you already granted and the agent has not used. -
The agent calls again with
approvalset to the id. itda checks the request matches what you saw. It marks the approval used and sends it once.
Until you answer, a call with the id gets approval_pending (exit 20) and the
command to run. The approval stays unchanged; the agent waits instead of asking
for a new one. A call with inputs other than those you saw withdraws the
approval (approval_expired with reason: inputs_changed). To use those
inputs, the agent asks again for you to review them.
An approval covers one request: the site, action, account, every input, the definition (by hash) and the credential (by fingerprint). Changing any of them needs a new approval.
A change that sends a file shows the file instead of the path input, by name, size and SHA-256:
$ itda approve 4c0d0f2e6b3b4ad8a0c1d9e7f5a2b3c4
attach-file wants to change Sandbox intranet (sandbox) as account default:
does Attach a file to a notice
request POST http://127.0.0.1:8787/board/42/attach
file plan.csv (8 bytes, sha256 3a1f0c…)
memo Q4 plan
Approve this change? [y/N] y
The approval is for those bytes. If the file under that path changes before
the agent calls again, the approval is withdrawn (approval_expired with
reason: file_changed) and the agent asks for a new one. If it changes while
itda is sending it, the send stops and the call ends with
action_outcome_unknown and reason: file_changed. The audit log records the
hash of the file a change sent, never its path or contents. It expires five minutes after the request or five minutes
after you approve it. itda uses an approval once, before sending anything, so it
never replays a failed change. itda logout, itda remove, a new itda login
and replacing a definition (itda setup, itda refresh, itda add --force,
itda draft ... --install --force) withdraw all pending or granted approvals
for that site or account: what you reviewed differs from what itda would send.
itda never retries changes or follows their redirects. If the answer is lost
after sending or a GraphQL answer carries errors behind a 2xx, the call fails
with action_outcome_unknown (exit 22). When the change names the read that
shows it (confirm in the definition, see SPEC.md), itda
runs that read once, as a read, and says what it found next to the error:
landed (the record is there: the change is in, and the agent is told not to
ask for it again), not_found (it is not: the agent may ask again and you
review a new approval) or unknown (the read failed too). Only the read’s name
and the match result are reported, never its answer. Without confirm, check
the site yourself before trying again. In no case does itda send the change
again. A change that never left the machine (the gate gave no turn, the name
did not resolve, the connection was refused) keeps its network code. itda
reports it as not sent.
After a landed check, asking for the same change with the same inputs still
needs a new approval, and its review carries a note line: when the earlier
one was sent and which read showed it landed. Approving sends it again; decline
if one is enough.
Approving in your MCP client
Clients with MCP elicitation forms can ask for approval there, without a switch to a terminal:
itda mcp install claude-code --approve-in-client # or set ITDA_APPROVE_IN_CLIENT=1
When an agent asks for a change, the client shows you the same review
itda approve prints, with an “Approve this change” box. Ticking the box and
accepting approves that one request. The call continues and runs it. Declining
or closing the form rejects it, and the agent gets approval_rejected. Both
decisions go into the audit log. If the client cannot show forms, nothing
changes. The agent gets the approval id; you can still use itda approve.
How the form reaches you depends on the protocol revision the client speaks. Up to 2025-11-25 itda sends the client a request and waits 290 seconds for an answer. If nobody answers, the form closes; the agent gets the approval id with seconds left of its five minutes. From 2026-07-28 the form is the call’s result. The client keeps it open as long as it likes and calls itda again with your answer. itda ignores answers that arrive after the approval expires and asks you again for a new approval.
What each client showed, measured on 2026-10-22:
- Claude Code 2.1.289 shows the form in the terminal, under “MCP server
“itda” requests your input“: the review lines, the box (“Approve this
change”), and Accept and Decline buttons. Space ticks the box, Enter on Accept
sends it, Esc closes the form. The agent waits (“Calling itda”) and sees
nothing until you answer. Accept runs the change; Decline and Esc both end in
approval_rejected. A four-line review showed whole. A longer one showed only its first three lines with “(+2 more lines)” and no key to show the rest; a change with several inputs may not show them all. Claude Code speaks protocol 2026-07-28 with itda. ItsElicitationhook can answer a form from a script without showing it. A hook answering itda’s form would approve changes on its own: do not add one. - Claude Desktop 3.1623.1 declares no elicitation capability, so itda never
sends it a form. Desktop shows its own “Allow once” card for the tool call.
The agent gets
approval_requiredwith the id; you approve in a terminal.--approve-in-clientchanges nothing there.
Turn this on only for clients that put elicitation forms in front of you. The form goes to the client application, never the model. A client that answers forms on its own would also approve on its own.
Why a terminal
Without the client option above, approvals come only from a terminal.
itda approve refuses to run when its input is not a terminal, and there is
no MCP tool that approves. An agent working through a pipe or an MCP
connection therefore cannot answer the question for you. This keeps honest
agents from approving their own changes. It is not a sandbox: software that
runs with your account and full shell access could drive a terminal or edit
the data directory. See the threat model.
The audit log
itda appends every call, approval, rejection and change to audit.jsonl in the
data directory:
$ itda audit
1 2026-10-19T08:43:01.846Z call sandbox.create-notice approval_required approval 9e96…
2 2026-10-19T08:43:01.895Z approved sandbox.create-notice ok approval 9e96…
3 2026-10-19T08:43:02.168Z change_started sandbox.create-notice sending approval 9e96…
4 2026-10-19T08:43:02.187Z call sandbox.create-notice ok approval 9e96…
$ itda audit --verify
The audit log is intact: 4 records in /home/you/.local/share/itda/audit.jsonl.
-
Changes are written down first. itda writes a
change_startedrecord before sending the request. If itda cannot write it, it sends no change. -
Tampering shows. Each record holds the hash of the one before it. Editing, deleting or reordering a line makes
itda audit --verifyname the first broken record, anditda doctorreports it. -
Safe to share. Records keep input names and a digest of their values, never the values or any credential.
-
A stopped process shows. A
change_startedrecord with no latercallrecord means the process stopped after sending the change. Nobody knows whether the site applied it.itda auditlists those changes at the end as “unknown outcome”.itda doctorcounts them underchanges.itda_manage auditreturns them underunknown_outcome. Check the site before asking for the change again. -
A lost answer shows with its check. A
callrecord ending inaction_outcome_unknowncarriesoutcome_check(landed,not_foundorunknown) andconfirm_readwhen the change named a confirming read.itda auditlists those changes among the records shown, each with what the read said;itda_manage auditlists them underunknown_outcomewithcheck.
itda audit --json prints the records for scripts.
Writing a site definition
A site definition (spec.yaml) tells itda a website’s actions, what each sends,
what a correct answer looks like and whether it changes anything. itda reads
<data dir>/specs/<site>/spec.yaml. Beside it, check.json stores the last
itda check time and verdict; sources.json stores a drafted definition’s
sources (see Keep it working). itda add spec.yaml copies
it there after a short review. itda doctor prints the data directory;
ITDA_HOME moves it.
Start from an OpenAPI document
When the site publishes an OpenAPI 3 or Swagger 2.0 description, let itda draft the definition:
itda draft openapi openapi.json > spec.yaml # print the draft
itda draft openapi openapi.json --include orders # only matching paths, ids or tags
itda draft openapi openapi.json --base-url https://api.example.com --install
The draft keeps each operation’s source in evidence and maps GET to reads,
all else to changes. It adds response checks from required fields in the success
schema and maps header API keys and bearer tokens to auth. itda lists what it
cannot translate (OAuth, cookie keys, header parameters, multipart bodies,
deprecated operations) on stderr without guessing. Review the draft before
agents use it: shorten descriptions, remove actions agents should not call, and
tighten the response checks.
Start from the pages you use
For a site with no API description, and pages you can open without signing in, give itda the addresses of the pages an agent should read:
itda draft page https://example.com/board > spec.yaml # print the draft
itda draft page https://example.com/board https://example.com/board/42
itda draft page https://example.com/board --name board --install
itda fetches each page under the same rules as every other read: once over
HTTP and, when the page answers with a challenge, once more inside the browser
installed on the computer (see HONEST_CLIENT.md). Private
addresses need ITDA_ALLOW_LOCAL=1. If the page still refuses, the draft stops
with what the gate saw, as itda call would report it: blocked_by_site (exit
12) naming Cloudflare, a bot manager or a captcha, or site_rate_limited (exit
13) with the wait the site asked for; nothing is paused. A page that sends itda
on to a sign-in page stops with auth_required (exit 10) and points at itda record. itda does not solve a captcha; the site’s official API, or its owner,
is the way in. Each page that answers becomes a read
with a response check, so itda check notices when the page changes:
| What the page has | What the draft gets |
|---|---|
| A table with a header row | html.items per row, one field per column named after its header, plus link |
A list of similar elements with a title and a link (cards, posts, tr.athing) | html.items with title, link and, when present, id, summary, meta, published_at, plus fields most records share at one location under a site-named class (score, author, release__version becomes version). Build names such as heading__fzQskI are left out. |
Three or more consecutive siblings of the same shape, outside navigation, sidebars, headers and footers, leaving out ad slots (an element whose class, id or label marks it as an ad: AdHolder, ad-*, sponsored, aria-label="Sponsored"). Each has a title in the same place: a link (ol.stories > li.story, #articles > dt, a dl per section), a heading, text named as a title (span.result__title), or the record itself if it is a link with content (a.release). | The same, reading title and link from the title’s own path in the page and date-like text as published_at. A heading or a link spanning the whole record needs one more field. Bare links, menus and paragraphs never qualify. If the page names its records, itda uses that name to describe the read (List the projects on Projects) |
A list of record links sharing one shape: one path with a different segment per record (/item/{id}), or one path with one varying query key (view.php?id=) | A one-record read (get-project, get-post) with a required id input (int for numeric ids). Query keys every link sends become inputs with defaults. No response check yet: run it once, then declare what the page shows. |
One thing (an article, a package, a product, a bug): an h1 outside the site’s header and menus, beside a body of text, a date, or two or more labelled values. It is outside any record list, or the list takes less than a quarter of the main block. | html.page fields across the page, each matching once: required headline; body (the longest named body block, div.project-description, #readme, [itemprop=articleBody], else the block with most text); fields beside the headline under site-named classes (project-header__summary becomes summary); description from page meta; published_at; author. Each dt/dd pair or th/td row adds a field named after its label. A page that is one thing with a named list of five or more records on it (a package with its releases) gets both reads |
A list in data embedded for scripts: an ItemList or a list of one type (NewsArticle, Product…) in ld+json. In __NEXT_DATA__, __NEXT_F__, __APOLLO_STATE__ (grouped by typename), __PRELOADED_STATE__ and similar data, choose the object list with the most to read (a title, a link, a date), then the longest among ties. | One read per list, named for its contents (list-news-articles), with ssr_json, extract_pointer to the list and required_pointers on its first record. Its model keeps up to eight plain fields per record, ids and titles first, excluding credentials. |
| Data embedded for its scripts with no list in it | ssr_json with required_pointers on the whole payload, and a note saying what to add |
| Only a title | required_html_fields on its h1 or title, and a note saying what to add |
Links to ?page=2 on the same path, or a link marked rel="next" with a number in its query | An optional query input named after that key (int, no default, so the read asks for the address it was drafted from until an agent or a page handle asks for another; counting starts at 1 for page and the like, at 0 for offset and start). A key the page’s own address carries keeps its value. The reads that return records also get pagination on that key (style: page, or style: offset for a key that starts at 0) |
A GET form | A read with the form’s fields as query inputs |
A POST form | A change with the fields as body.form, using auth: session; a hidden token such as _csrf becomes a preflight step read from the form page |
A POST form with enctype=multipart/form-data and a file control | A change with the fields as body.multipart.fields and the file control as body.multipart.file, with an input of its own for the path of the file to send |
Links the page labels as attachments: a download attribute, the words download, attachment, 첨부 or 다운로드 in the link’s text, title, label or class, or an icon named for one | A file read (download-file) with response: {format: file}. Links of one shape become one read with an input (/files/{file_id}, or a varying query key); media_types lists the kinds the link names say, or the usual document types when they say nothing. A link to a document the page does not call an attachment stays a page |
| A sign-in form | Left out; itda setup --user and itda login <site> sign in through it, see auth below |
<link rel="alternate"> to RSS or Atom | A read with format: xml |
A link to an OpenAPI document, in the page’s Link header or a <link> or <a> (rel="service-desc", an API catalog with rel="api-catalog", rel="describedby", or an href ending in openapi.json, openapi.yaml or swagger.json), or, when nothing is linked, /openapi.json on the same origin answering with one | Its reads join the draft as itda draft openapi makes them, under the page’s address. Its changes appear as notes. See below. |
When a site also publishes an API description, itda reads it: the one the page
points at, else /openapi.json on the same origin when it answers with an
OpenAPI 3 or Swagger 2.0 document. It makes one GET as itda, at most 4 MB. Every
request to a site waits its turn, so no other path is tried. Where the page
points comes from the HTTP Link header the first page was answered with
(RFC 8288) and from the <link> and <a> elements of the pages, which costs
no request. The best wins: service-desc (RFC 8631) or a description file
name, then api-catalog (RFC 9727), then describedby; on a tie the header
comes first. A service-desc document is read even when it does not parse,
so the draft can say so. An API catalog is a linkset (RFC 9264,
application/linkset+json): itda follows its service-desc links, up to
three, and takes the first OpenAPI document; a catalog that only points at
further catalogs is not followed. A describedby document counts only when it
is an OpenAPI document; otherwise /openapi.json is still tried. Its reads become
actions as itda draft openapi makes them. Each path is drafted once, whether
the page or description named it. The changes it lists are notes rather than
actions (one each up to ten, then a count); itda draft openapi <url> drafts
those for review. itda omits and lists reads with credential inputs, such as an
apikey query parameter. Install-time checks would refuse them. itda only notes
descriptions with another server address; it does not merge them. A description
that does not parse leaves the page draft unchanged. The header comment and
every merged action’s evidence name the description’s address.
Field and input names face the same checks as a hand-written definition: itda
omits and lists names that look like credentials. A hidden form field other than
a token keeps its value as a default: the form always sends it. itda copies
nothing else from the page. A script-drawn page shows little to a plain fetch.
itda draft page reports this and points to recording the site; itda setup
first watches the page load in your browser (below).
A fetched page is a shell when its main content is one or more same-origin
<iframe src> (or an older <frameset>’s <frame src>) and it has no records,
data, form or feed of its own. itda then fetches each frame’s page as a read, on
the same pacing and with the same session, and drafts from the frame pages as
pages of the site. Each frame read keeps the frame’s own address as its
operation path, since that is what a request to the server looks like. A form
inside a frame posts to the frame’s action like any other. itda follows at most
four frames, at most two deep. itda lists a frame on another origin in the notes
and never fetches it. The same goes for a same-origin frame that answered with
nothing. itda draft page and itda setup say what they followed (“Followed 1
frame: /frame/board.”).
When a same-origin frame answers with a redirect to a sign-in page (the same
detection the top-level page uses), itda treats it as if the top-level page
had: following the redirect to read the login form is a read, so with
itda setup --user it signs in through that form and reads the frame again
with the session, then drafts from it. itda draft page, which does not sign
in, notes that the frame leads to a sign-in rather than only that it was not
read.
Start from the software a site runs
Most intranets run software everyone knows, and that software has a stable API
worth more than a reading of one page. itda setup recognises eight of them
from page HTML (a generator meta, a well-known asset path, an Atlassian
context-path meta, a body class). It confirms the guess with exactly one GET to
the software’s own endpoint, as itda, through the same gate as other requests.
No fingerprint, no request. A confirmed match says so in one line, “This looks
like GitLab. Using its API at sites/software/:
| Software | Recognised by | Confirmed with | API base | Signs in with |
|---|---|---|---|---|
| GitLab | og:site_name GitLab, gl-* body classes, /assets/webpack/ scripts (their prefix is the relative URL root) | GET /api/v4/projects?per_page=1 | <root>/api/v4 | PRIVATE-TOKEN |
| Jira (Data Center, Cloud) | application-name JIRA, ajs-context-path | GET /rest/api/2/serverInfo, whose baseUrl becomes the base | <baseUrl> | the Authorization header you store: Bearer <PAT> or Basic <email:token> |
| Confluence | confluence-base-url / confluence-context-path metas | GET /rest/api/space?limit=1 | <context> | the same |
| Redmine | controller-* and action-* body classes with /stylesheets/application.css (its prefix is the sub-directory) | GET /projects.json?limit=1 | <root> | X-Redmine-API-Key |
| Discourse | generator Discourse | GET /site/basic-info.json | origin | reads only (posting needs two headers a definition cannot send yet) |
| MediaWiki | generator MediaWiki or the mediawiki body class; EditURI names api.php | GET api.php?action=query&meta=siteinfo | the script path (/w on Wikipedia) | the browser session, for editing |
| WordPress | <link rel="https://api.w.org/">, else generator WordPress | GET <wp-json>/ listing wp/v2 | the linked API root | an application password, as basic auth |
| Gitea, Forgejo | their default keywords meta; appSubUrl in window.config | GET /api/v1/version (a +gitea- version is Forgejo) | <sub>/api/v1 | Authorization: token |
Each template holds eight to fourteen reads an agent needs most (lists, search,
one item), up to three marked changes, record models and the software’s sign-in
method. Every operation has its documentation in evidence. itda names the site
after the host (issues.apache.org becomes issues-apache; --name
overrides). The template’s store:<software>/... secrets follow that name. itda
tries every read once, as on any site, and removes reads the instance does not
answer (a private project list, a disabled module). The page draft still runs.
Its lists, forms and feeds join the template under the page’s address if it
differs from the API’s. Each method and path is drafted once. A page that is not
enough on its own, because it needs a sign-in or is drawn by scripts, still gets
the template when the software is confirmed. If the page says WordPress but
/wp-json/ does not answer like WordPress, itda says nothing. It drafts the
page like any other.
itda setup --as <software> forces a template, using the fingerprint’s base if
the page agrees, else the API under the page’s origin. The confirm request still
runs; the line says if it got no answer. --no-template reads the page like any
other. itda check and itda refresh work on the result as on any site. A
refresh fills the same template at the same API base and merges the page’s reads
again. Over MCP, itda_setup does the same. It takes template (a name or
none) and names the template used in its result. itda add does not install
the templates: setup fills their placeholder base_url.
Sites with a ready-made definition
A page on pypi.org, github.com, www.npmjs.com (or npmjs.com) or
crates.io gets the site’s itda add definition (pypi, github, npm,
crates-io). It reads the site’s own API instead of its pages. itda setup
says so in one line, “This is PyPI. Using its JSON API.”, and does not fetch the
page. It saves the definition under its own name and tries its reads once as on
any site. --no-template reads the page like any other; --as <software> uses
that template instead. Over MCP, itda_setup takes the same path, with
template: none to skip it, and puts the line in its notes. A site set up this
way has no sources to redraft from, so itda refresh does not apply.
itda add <site> --force installs the current definition.
When setup tries the reads
itda setup tries every read once while you watch, over HTTP and then, when
the site answers with a challenge, once more in the installed browser. A read
the site still blocks or challenges (a 403 from a bot manager, a captcha, a 200
page titled as a challenge) is a finding of the setup, not a refusal to
remember: that read is left out with one line, “itda check tries them. The site is not paused,
since nothing was retried. itda installs reads that already answered.
itda check, itda call and the MCP tools on an installed site try a
challenged read in the browser as well; only in direct-HTTP mode
(ITDA_TRANSPORT=http) does a block pause the site until you resume it.
Start from an agent’s reading of the docs
When the pages say little but the site documents its API in prose, an agent
connected over MCP can draft the definition itself with itda_prepare
(agents.md has the loop). itda reads the pages and documents and
gives the agent their shapes and an id for each piece of evidence, never a
value. The agent proposes a definition in this format; itda checks it with
the same rules itda spec validate applies and keeps an operation only when
the evidence carries it:
| The operation | Kept when |
|---|---|
base_url | a server the site documented (OpenAPI servers, an absolute address in its docs) or answered from |
method and path | a route in the docs, the description or a script that answered, matched across /api and version prefixes; or, for a GET, a path itda read or saw a page link to |
| an HTML read | an html extraction, records (items with fields) or page fields (page); with required_html_fields alone the read has nothing to return and fails (response_mismatch) |
| a file read | response.format: file with media_types |
| a signed-in operation | a path read with the account’s session |
| a declared read | its request, rendered as it would be sent, asks for no change; one that does is kept as a change |
A route the description says needs a sign-in itda cannot use is dropped, and
a draft that only reads the home page and checks nothing is refused. itda then
tries the kept reads through the same path as any call, at most 24 requests
in a session: a JSON read without required_pointers gets them from the
field names at the top of its answer (none for a list, or for an object keyed
by data such as languages or dates), and one that answers with nothing or
only an error does not count. A change is never sent. Installing takes the same steps
as itda setup: save, try every read once, keep what works. The header says
the definition came from itda_prepare; review it like any draft.
Start from a browser recording
Most internal and older sites publish no API description, but their pages still call one. Record yourself using the site and let itda draft from the recording:
itda record https://intranet.example.com/board # print the draft
itda record https://intranet.example.com/board --name board --install
itda record https://intranet.example.com/board --save session.har # keep the recording too
itda opens Chrome, Edge or Chromium (or ITDA_BROWSER) in a fresh, empty
profile at that page and watches, through the browser’s own DevTools protocol,
what every page asks the server for. Use the site the way an agent should: sign
in, search, open an item, submit a form. Press Enter in the terminal or close
the window when you are done. itda never clicks, types or navigates in that
window; it opens only the page you named. The recording stays in memory unless
--save writes it to a file. That file holds your cookies and tokens; delete it
afterwards. With --install, itda offers to keep the recording’s sign-in token
for a drafted header auth entry and session for a browser entry. Each needs
a yes in the terminal. They go to the system keyring under <site>/default and
never appear on screen. You need itda login only when a token expires.
itda setup runs a shorter version automatically when a page shows itda only a
title and has scripts that could fetch data: a script file, preloaded module, or
inline script that imports a module or mentions fetch, XMLHttpRequest,
graphql or api. Without these, the page goes straight to the question below.
It opens the same kind of window on the page, waits for two seconds of network
silence (ten at most), scrolls to the bottom once and waits again. It closes the
window and drafts from what the page loaded on its own, like itda draft har.
It tries the page’s own address first, so what the page itself shows is never
lost to a busier API elsewhere. It reports one line
(Watched the page load: 12 calls, 3 reads drafted., counting reads beyond a
title check). It asks you to record only if it drafted no such read. It never
clicks or types in the window or keeps sign-in data, since nobody signed in. A
page whose scripts lead to sign-in stops with auth_required, as a redirect
does. --no-record skips the window as well, --watch-only watches but never
asks you to record. itda uses the template for a site with recognised software
(above) and does not watch it.
A browser developer-tools recording works the same way. Open the Network tab, use the site, save with “Save all as HAR” (Chrome, Edge) or “Save All As HAR” (Firefox), and draft from the file:
itda draft har session.har > spec.yaml # print the draft
itda draft har session.har --base-url https://intranet.example.com/api
itda draft har session.har --name board --install
A GraphQL page sends every call to one path; each recorded operation becomes an
action named after it (exploreProjects is explore-projects), with queries as
reads and mutations as changes. The document and operation name are fixed
inputs; agents fill in variables, which a json_template renders into the same
JSON the page sent. itda lists variables holding objects or lists for you to add
by hand.
A GraphQL endpoint can also say what else it offers. When a draft finds an
endpoint in a recorded call or a page naming a same-origin /graphql path, itda
sends the standard introspection query once, as a read with a size cap. It drafts one read per root Query field with scalar, enum or id
arguments: the field name is the action (countries), arguments are typed
inputs (required if the schema says so), and the selection contains up to 24
scalar fields, two levels deep. itda omits and lists optional object or list
arguments, so it can still read the field without them. It lists fields
requiring an object, list or credential instead of drafting them. An argument
named query becomes the input <field>_query, since query holds the
document. itda lists Mutation fields in notes, never drafting them as changes:
a schema says what a change takes, not what it does. itda drafts sites with
introspection off from their pages and calls alone, with no error. This is the
one request itda draft har makes on its own.
In both cases, itda also drafts the pages you saw, as itda draft page does.
Sites rendering lists and forms on the server get actions even when their pages
call no API. A recording holds real tokens, cookies and personal data, so the
draft keeps only the shape of each call: method, path, input names and types,
and the response fields every sample returned. Numbers and ids in paths become
placeholders (/notices/42 becomes /notices/{notice_id}). itda marks inputs
every sample sent as required. Token headers become auth entries whose values
you add with itda login. Calls with a sign-in cookie (by name, such as
JSESSIONID or SID) but no token get a kind: browser entry.
itda login <site> --browser signs them in. A form token such as _csrf
becomes a preflight step. It reads the token from the form page recorded
before the post, so each change sends a fresh one. A recorded
multipart/form-data post whose parts the browser kept (their names, and the
file part’s file name, never the file) becomes a change with body.multipart;
a recording without the part names lists the call instead. An answer the site
sent as Content-Disposition: attachment becomes a file read of the media type
it carried. A header such as
X-CSRF-Token gets the same treatment using a <meta name="csrf-token"> tag on
an earlier page, as Rails, Laravel and many single-page apps do. A session the
site carries in the URL rather than a cookie also gets a kind: browser entry,
with value_from: query:<key>. itda drafts a query key as the session when the
sign-in’s answer hands it out (in the redirect Location, or on the links of
the first page after the sign-in), its value looks like a session, and every
signed-in call carries it. Every operation that sent the key gets
auth: <name>, and itda drops the key from each operation’s inputs. itda keeps
the value in the keyring, never in the definition. At run time, itda adds it to
each request’s query, just as it sends a cookie: session as a cookie. itda
leaves out the sign-in call: a person signs in, never an agent. itda omits and
lists other token and session fields, since they come from sign-in, never the
agent. itda skips calls to other addresses and failed calls. Delete the HAR
file when you are done with it.
Write one by hand
This page covers everything version 0.1 runs. The fastest way in is to copy
examples/sandbox/spec.yaml and change it.
A complete example
spec_version: 1
site:
name: sandbox # lower case, digits and '-'; used in <site>.<action>
title: Sandbox intranet # shown to people and agents
base_url: http://127.0.0.1:8787
auth:
- name: key
kind: header
header: X-Api-Key
value_from: store:sandbox/api-key # filled by `itda login sandbox`
operations:
- name: list-notices
desc: Search notices by title
method: GET
path: /api/notices
effect: read
evidence: GET /api/notices returns {total, items}
query:
q: {type: string}
response:
format: json
required_pointers: [/items, /total]
- name: create-notice
desc: Post a notice
method: POST
path: /api/notices
auth: key
effect: write
evidence: POST /api/notices creates a notice
body:
json:
title: {type: string, required: true}
itda ls lists what it found, and a broken definition shows up there and in
itda doctor with the problem and how to fix it.
To check a file before installing it, run itda spec validate spec.yaml. It
runs the checks itda add runs (the structure, the addresses, confirm and
the rest) and installs nothing. A valid file exits 0; otherwise it prints one
problem: <field>: <detail> line per problem and exits 2, the code of every
definition problem. --json prints {status, file, site, actions, issues},
each issue {code, field, detail}; a file that does not parse prints the
usual error envelope.
site
| Field | Required | Meaning |
|---|---|---|
name | yes | Id used on the command line and in MCP tool names. |
title | no | Human name, used in tool descriptions and approval prompts. |
base_url | yes | https:// origin every path is joined to. Local and private addresses also need ITDA_ALLOW_LOCAL=1. |
defaults.headers adds headers to every request (for example an Accept
language). Browser-controlled headers (sec-*, client hints) belong to the
transport and cannot be supplied by a definition;
see HONEST_CLIENT.md.
defaults.interval sets the minimum time between requests to a site, written as
2s or 500ms. itda spaces action-request admissions to an origin about 1.2 seconds
apart by default; a definition can increase this to 30 seconds but cannot reduce
it. Use it when the site’s terms name a rate, or when itda check keeps running
into site_rate_limited.
defaults:
interval: 2s
operations
Each operation is one action, called as itda call <site>.<name>.
| Field | Meaning |
|---|---|
name | Action id: lower case, digits and -. |
desc | One line an agent reads to decide whether to use it. Write it as an instruction: “Search notices by title”. |
method | GET, POST, PUT, PATCH or DELETE. Defaults to GET. |
path | Path under base_url. itda replaces {name} with a path input. |
effect | read or write. See below. |
evidence | Why you believe the effect is right: a doc link, a captured request. Reviewers read it. |
auth | Name of an auth entry, for actions that need an account. |
params | Path inputs, all required. |
query | Query string inputs. |
body | Request body inputs: json: or form: fields, a json_template, or multipart: for a form that sends a file. See below. |
headers | Extra headers for this action only. |
response | What a correct answer looks like. Strongly recommended for reads. |
model | Name of an entry in models that picks and renames fields from the JSON. |
pagination | How a read that lists things goes on to its next page. See below. |
confirm | For a change: the read that shows it landed, run once when the change’s answer is lost. See below. |
Inputs
query:
q: {type: string}
page: {type: int, default: 1}
draft: {type: bool, required: true}
type is string, int, float or bool. itda checks inputs before sending
anything; agents see them as a JSON Schema. itda refuses inputs with names
resembling credentials (password, token, api_key…): those go in auth,
never through the agent.
Sending a file
A form that takes a file is a multipart/form-data body. fields are its
text inputs, declared like form fields; file names the form field the file
goes in and the input (from) that carries the path of a file on this
computer:
operations:
- name: attach-file
desc: Attach a file to a notice
method: POST
path: /board/{id}/attach
auth: session
effect: write
evidence: the attach form on /board/42 posts multipart/form-data
params:
id: {type: int}
body:
multipart:
fields:
memo: {type: string}
file: {name: attachment, from: file}
The agent gives file as a path. itda reads the file once, and the approval
the user sees names it by name, size and SHA-256 (file plan.csv (8 bytes, sha256 3a1f…)). The approval is for those bytes: other bytes under the same
path withdraw it (approval_expired, reason: file_changed), and the agent
asks again. itda streams the file from disk in small chunks, checking its size
and modification time before every chunk and its hash at the end. A file that
changes during the send stops it; the call ends with action_outcome_unknown
and reason: file_changed, like any change whose answer was lost. The audit
log records the hash. An action with a file is always a change, whatever its
method or name says, and its body goes only to the site’s own origin. A
multipart body without a file sends its fields as parts. multipart is
one of the three body encodings; declare one of json, form or multipart.
Read or write
An action is a read only if effect is read, its method is not PUT, PATCH
or DELETE, and neither name nor path has a change word (delete, send,
pay, submit, logout…). It must also have no input such as action or
mode asking for a change, and no mutation in a GraphQL query input. A POST
can be a read, for example a search endpoint or a GraphQL query. Anything else
is a change. itda sends a change once, without retries, only after the user
approves that exact request with itda approve.
When the checks disagree with your effect: read, itda treats the action as a
write. Rename the action or path rather than fighting the classifier: a
GET /logout really is a change. itda refuses headers and inputs that ask the
site to use another method or address (X-HTTP-Method-Override,
X-Method-Override, X-HTTP-Method, _method, X-Original-URL,
X-Rewrite-URL). It sends the reviewed method to the reviewed address. itda
refuses transport other than http and auth kinds other than none,
header and browser. How a read travels (direct HTTP, or the installed
browser after a challenge) is itda’s choice, not the definition’s; see
HONEST_CLIENT.md. A change always goes over direct HTTP.
Those checks look at what the definition declares. Just before sending, itda
reads the request it built once more: every query key and body field, the
value of a field such as action or mode, and any GraphQL document in
query (the body, or the address of a GET). A read whose rendered request
asks for a change, for example a json_template whose literal body is a
mutation or carries {"op": "delete"} deep inside, is reclassified as a
change: the call returns approval_required with reclassified naming the
reason (body names a mutation, body asks for a change, address names a mutation, …), the review shows the same line, and the user decides as for
any change. A read whose effect itda cannot tell, such as a GraphQL endpoint
sent no document, or a document it cannot read, stops with effect_unknown
(exit 2) and a remedy; nothing is sent. A declared write never gets looser
this way. A search whose text happens to contain a change word stays a read:
only keys, intent fields and GraphQL documents are read.
response
A declared response is how itda knows the answer is real data and not a sign-in page, an error page or a redesigned site.
response:
format: json # json | html | text | xml | empty | file
required_pointers: [/items, /total] # JSON Pointers that must exist
For records inside a wrapper, extract_pointer returns only that part.
required_pointers and model then apply to it:
response:
format: json
extract_pointer: /items # {"total_count": 3, "items": [...]} -> [...]
For server-rendered pages, extract records with CSS selectors:
response:
format: html
required_html_fields:
- {selector: "h1#board"} # must be on the page
html:
items: tr.notice # one record per match
fields:
id: {selector: td.id, required: true}
title: {selector: td.title a, required: true}
link: {selector: td.title a, attribute: href, absolute_url: true}
limit: 100 # at most this many records
When the answer does not match, the call fails with response_mismatch (exit
11) instead of returning the wrong page as data.
Reading a file
A read whose answer is a file to keep, an attachment on a board or a report
to download, declares format: file and the media types it accepts:
operations:
- name: get-attachment
desc: Download an attachment from a notice
method: GET
path: /board/files/{file_id}
effect: read
evidence: /board/42 links to it as an attachment
params:
file_id: {type: int}
response:
format: file
media_types: [application/pdf, application/vnd.ms-excel]
max_bytes: 20000000 # optional; 64 MiB when left out, 1 GiB at most
allow_empty: false # optional; an empty answer is a failure
itda streams the answer to a file instead of reading it into memory. The
answer’s content type (before any ;) must be one of media_types; an HTML
answer counts only when the site marks it Content-Disposition: attachment,
so a sign-in page in place of the file is a failure, not a file. A 206 or an
answer with Content-Range, a body shorter than its Content-Length, an empty
body (unless allow_empty) and anything over max_bytes fail too, by name:
wrong_media_type, partial_answer, empty_answer, too_large (exit 11, or
30 for too_large). Nothing is kept from a failed answer. A good answer is
sealed at its end and gets its name from the site’s Content-Disposition, else
the address, reduced to plain characters; a name already taken gets -2, -3.
The file lands under files/<site>/ in the data directory, or in the folder
itda call ... --out <dir> names. The read returns a manifest, never the
bytes:
{"path": "/home/you/.local/share/itda/files/board/minutes.pdf", "bytes": 48213,
"media_type": "application/pdf", "sha256": "9c1e…"}
Over MCP the result carries the same manifest and a resource_link to the
file. A file read has no model, pagination, pointers or extraction, and
itda check skips it (“saves a file”) rather than fill the folder. itda setup tries it once, through the gate like every read: it reads the answer’s
headers and first bytes, counts the read as working when they describe a file
the definition allows (status, media type, size), and drops the rest; nothing
is saved. A transfer
longer than the pace allows keeps its turn at the gate by renewing it; a turn
lost mid-transfer stops it (request_lease_lost, exit 30), never retried.
auth
API keys and tokens
A key or token sent in a header:
auth:
- name: key
kind: header
header: Authorization
prefix: "Bearer "
value_from: store:<site>/api-key
Store the value with echo "$KEY" | itda login <site> (OS keyring), or set
ITDA_SECRET__<SITE>__<ACCOUNT>__<NAME> for CI and containers. --account
picks among several accounts; the default account is default. itda sends the
header only to base_url’s origin, or to origin when you set it.
Sign in with a password
For a site with its own sign-in form, name the form on the browser entry
and itda fills it in for you:
auth:
- name: session
kind: browser
value_from: cookie:JSESSIONID # the cookie that shows you are signed in
login:
path: /login # the page with the form; a query is fine
user: username # the user name field
password: password # the password field
$ itda login intranet --user me
Password for me on intranet (sent once to the site's own sign-in form, never saved):
Signed in. itda sealed the session for intranet/default (2 cookies) under the data directory with a key in the system keyring; agents only see the account id.
itda reads the page at path, like any other read, and selects the same-site POST form
with those two fields. It fills them in and sends the form once, as your browser
would, with the page’s hidden fields (a token such as _csrf, a return address)
and cookies. A redirect means sign-in succeeded. itda stores the site’s reply
cookies as the session, as for a browser sign-in. If the form comes back,
sign-in failed. itda does not retry or pause anything. itda sends the password
only to the form’s own action and never keeps it. Without a terminal, the
password is the first line of stdin, so a password manager can pipe it.
itda setup <page> --user me does the same if the page needs a sign-in
(redirects to one, answers 401 or 403, or says “log in to see this”). It drafts
from the page as you see it signed in. Every action gets auth: session; it
writes the login block from the form it found so the command above works
later. An agent never gets this path: itda_setup over MCP refuses pages behind
a sign-in and says what you should run.
Some older sites keep the session in the address instead of a cookie, as a
query parameter the sign-in hands out (?sid=...). Name that key with
value_from: query:<key> in place of the cookie:
auth:
- name: session
kind: browser
value_from: query:sid # the query key the signed-in session rides in
login:
path: /login
user: username
password: password
itda signs in the same way; when the site answers with no session cookie but a
redirect (or a first page) carrying sid, that value is the session. itda keeps
it in the keyring like any other. At run time it adds it to the query of every
request to the site, the way it sends a cookie session as a cookie. Because it
travels in URLs, it can reach a server’s or a proxy’s logs; see
THREAT_MODEL.md. A browser entry carries one or the other,
a cookie or a query key, not both.
A session a single-page app keeps in storage
Some sites keep no session cookie at all. A single-page app signs you in, puts a
token (often a JWT) in the browser’s localStorage, and its scripts send it on
every call as Authorization: Bearer <token> or a custom header. That is a
header credential whose value is a session, so its value_from names the
storage key instead of the site’s own store:
auth:
- name: session
kind: header
header: Authorization
prefix: "Bearer "
value_from: storage:access_token # the localStorage/sessionStorage key
When you set the site up or record it, itda reads that one origin’s
localStorage and sessionStorage once, over the DevTools protocol, from the
window you signed in to. It keeps a value only when the recorded calls sent it
as a header; a stored value no request sent is reported and left alone (“the
site keeps a value in storage that no request sent; not used”). The token goes
to the keyring as the session, never into the definition. itda login <site> --browser reads it again from the window at the next sign-in, the way a cookie
session is refreshed. itda reads only the signed-in origin’s storage, never
another site’s and never a file on disk, and never writes anything into a page.
If the token is a JWT with an exp claim, itda records when it expires:
itda check reports signed_out (not site_changed) once it has passed, and
itda_sites shows when the sign-in expires. itda never signs in again on its
own; it asks you to. value_from on a header entry accepts store:<site>/<key>
or storage:<key>.
When the site redirects after a change (post, then redirect, then get, as older
sites do), the change has landed. Its result carries the redirect address. A
redirect to a sign-in page means the site no longer accepts the session. The
call stops with auth_required; itda login <site> signs in again.
Browser sign-in
For single sign-on, a one-time code, or a site whose form itda cannot fill in:
auth:
- name: session
kind: browser
value_from: cookie:JSESSIONID # optional: the cookie that shows you are signed in
Sign in once in your own browser:
$ itda login intranet --browser
Sign in to intranet in the browser window that just opened.
itda continues when the JSESSIONID cookie appears, or press Enter once you are signed in.
Saved the session for intranet/default (3 cookies). It is sealed under the data directory with a key in the system keyring. Agents only see the account id.
itda opens Chrome, Edge or Chromium (or ITDA_BROWSER) in a fresh, empty
profile at the site’s address, or at --url. You sign in by hand; itda never
clicks or types on the page. It keeps only the site’s unexpired cookies. It
closes the window and deletes the profile. itda sends the session only to
addresses on the same site and removes cookie values from results.
A session is a whole Cookie header, too long for some credential stores
(Windows Credential Manager caps a credential at 2560 bytes). itda stores it in
vault.sealed under the data directory, encrypted with a system keyring key.
API keys stay in the keyring itself. The move is automatic in both directions,
and itda doctor says where secrets are.
When the session expires, the site answers 401, or 403 with a reason such as
session_expired. The account pauses with auth_required;
itda login <site> --browser signs it in again and resumes it. A 403 with any
other reason is api_denied: the site did not allow the request. Signing in
again will not help. On a server without a keyring, set
ITDA_SECRET__<SITE>__<ACCOUNT>__SESSION to a Cookie header value instead.
preflight
Older sites often hand out a one-time value with the form page, such as a CSRF token in a hidden input, and refuse a change without it. A preflight step reads that page just before the action and passes the value along:
operations:
- name: post-notice
method: POST
path: /board/write.do
auth: session
effect: write
evidence: the write form posts _csrf from /board/writeForm.do
preflight:
- name: form
path: /board/writeForm.do
extract:
csrf: "css:input[name=_csrf]@value"
headers:
X-CSRF-Token: "{{step.form.csrf}}" # if the site wants a header
body:
form:
title: {type: string, required: true}
_csrf: {type: string, default: "{{step.form.csrf}}"} # or a form field
extract source | Takes |
|---|---|
css:selector | the text of the one element the selector matches |
css:selector@attribute | that element’s attribute |
json:/pointer | a value from a JSON answer |
header:name | a response header |
cookie:name | a cookie the answer sets |
ssr:key#/pointer | a value in the JSON a page embeds for its scripts: ssr:__NEXT_DATA__#/buildId, or ssr:auto#/pointer for the first payload found. Only a string, number or boolean counts; an object or a list stops the call |
A step is a GET to the action’s own origin, with the same account, either to
path or to a read action named in op (with args). Steps run in order after
you approve a change, just before itda sends it. This keeps the value fresh.
itda sends cookies from a step’s answer with the action, as a browser does, for
that request only; it never keeps them. A Rails site keeps its issued token in a
session cookie it rewrites on every answer. A value is used as
{{step.<step>.<name>}}. Agents cannot see or set inputs filled this way. If a
step finds nothing, itda stops the call with response_mismatch and sends
nothing. After the steps, itda classifies a read again with the values filled
in. If it now counts as a change, itda stops with effect_changed (exit 2)
without sending it.
models
A model picks and renames fields from a JSON answer, so agents get small, stable
records. For a list answer, itda picks each record the same way.
itda draft page writes one for every list it finds in a page’s embedded data;
path reaches into nested objects with dots (item.headline):
models:
notice:
id: {path: id, type: int}
title: {path: subject}
by: {path: author.name}
operations:
- name: list-notices
model: notice
...
pagination
A read that lists things can say how the site turns pages. The agent can then read on without seeing the site’s own marker:
operations:
- name: search
method: GET
path: /api/v1/crates
effect: read
evidence: https://crates.io/data-access#api
query:
q: {type: string, required: true}
page: {type: int, default: 1}
pagination: {style: page, next: /meta/next_page, send: {query: page}}
| Field | Meaning |
|---|---|
style | page (a page number), offset (how many records to skip) or cursor (a marker the site hands out). |
next | Where the next marker is in the answer: a JSON pointer such as /meta/next_cursor or /data/notices/pageInfo/endCursor, or header:link for the rel="next" address in the Link header. A page or offset read may omit it. If the answer has records, the next marker is the next page number or the offset past those records. |
more | A JSON pointer that must hold true for another page to exist, such as /data/notices/pageInfo/hasNextPage. Optional. |
send | The input the marker goes back in: {query: page} or, for a JSON body, {body: after}. Declare the input as an int for page and offset, or a string with no default for cursor. |
When the answer says there is another page, the call returns meta.next, an
opaque handle such as ic_4f0a4207d04f4a0fb4e059d26bd54b6d. itda keeps the
marker on the user’s computer for fifteen minutes, bound to the site, action,
account, inputs and definition. itda call ... --next <handle> (over MCP,
next) sends it back in the declared input. Agents never see or set a cursor
input; they can still give page and offset inputs directly. For a pointer
holding an address or query string (?page=2&per_page=10), itda extracts only
the input’s value, and only if the origin and path match. itda drops a marker
that would change the read or point elsewhere. Only a read turns pages.
itda draft openapi declares it when the document is plain about it: a
cursor, page_token or after query parameter together with a next_cursor,
next_page_token or end_cursor string in the answer; a page parameter, with
header:link when the answer documents a Link header; an offset parameter.
itda draft har does the same from the query a recorded read sent and the
fields its answers had, and from a GraphQL after variable with
pageInfo.endCursor in the answer. itda drafts GraphQL schema connections
(after: String in, pageInfo { endCursor hasNextPage } out) with
send: {body: after}. Pages with pagination links (?page=2) get style: page
on reads returning their records.
confirm
itda sends a change once and never resends it. When the answer is lost after
sending (a timeout, a dropped connection), the call fails with
action_outcome_unknown and nobody knows whether the site applied it. A change
can name the read that would show it, so itda checks instead of guessing:
operations:
- name: list-notices
method: GET
path: /api/notices
effect: read
evidence: GET /api/notices answers {items: [{id, title}]}
response:
format: json
required_pointers: [/items]
- name: create-notice
method: POST
path: /api/notices
effect: write
evidence: POST /api/notices creates a notice; GET /api/notices lists it under title
body:
json:
title: {type: string, required: true}
confirm:
read: list-notices
match: {pointer: /items/*/title, from: body.title}
| Field | Meaning |
|---|---|
read | A read in the same definition, with effect: read, that runs on its defaults alone (no required input without a default, no path input). |
match.pointer | A JSON pointer into the read’s answer as itda returns it, after extract_pointer and model: /items/*/title for records under items, /*/title for a list. * stands for every element. HTML reads return their records under items, so the same shape works for them. |
match.from | The change’s input to compare with, as body.<name>, query.<name> or params.<name>. A string or number input that is required or has a default; never one a preflight step fills. |
After action_outcome_unknown, itda runs the read once, as a read: no
approval, through the same gate and pacing, with the change’s account, first
page only. It compares each value at the pointer with the input as text and
reports one of three results next to the error, as outcome_check in
--json and MCP results and as outcome_check on the audit record:
result | Meaning | What follows |
|---|---|---|
landed | the input is in the read’s answer | the change is in; itda tells the agent not to ask for it again. A new approval request with the same inputs carries a note saying the earlier one landed, so you can decline it |
not_found | it is not there | the normal path: the agent may ask again, and you review a new approval |
unknown | the read failed too (error says why) | check the site before asking for the change again |
The result stays action_outcome_unknown (exit 22) in every case; the check is
information next to it, never a reason to resend. The read’s answer is not
passed on, only the read’s name and the match result. A confirm that names a
missing operation, a change, or a read that needs inputs is a definition
problem, reported by itda ls and itda doctor like any other.
itda setup and itda draft fill confirm when the draft itself shows it: a
POST whose list read is on the same path or above it (POST /api/notices
with GET /api/notices, POST /issues/new with GET /issues) and a string
input of the change that the read’s records carry under the same name (an
HTML read’s fields, a model’s fields, or /0/<field> in
required_pointers). The evidence says so. Without that, the change keeps no
confirm; add one by hand when you know the read.
Keep it working
Sites change. itda check runs every read needing no input; give inputs a
default to include more. It reports whether each action’s answer still matches
its response:
$ itda check board
board.list-notices ok HTTP 200 142 ms
board.get-notice skipped needs notice_id
board.post-notice skipped changes the site
board.my-messages auth_required sign in: itda login board
verdict signed_out: sign in again with itda login board
1 ok, 1 failed, 2 skipped.
itda sends no changes. Each site gets a verdict (ok, signed_out,
site_changed, blocked or unreachable) and a matching exit code, per the
contract. Code 11 means a site no longer matches its definition,
letting a scheduled job warn you before an agent runs into it.
itda check --all checks every installed site and prints one verdict line each.
An address the site answers with 404, 405 or 410 and a query it answers with
errors count as site_changed. itda lists a refused read (api_denied) on its
own line; it does not affect the site’s verdict.
What itda knows about a site
itda keeps a small knowledge base per site in itda.db: the pages it drafted
from and the actions in the definition, which list read can supply which
detail read’s input, every try (from setup, check, refresh or a call) with its
verdict, HTTP status and time, and the hashes of spec.yaml and the last
verdict. It keeps shapes only: selectors with match counts, field names, the
keys and types of an answer. Never an answer, an input, a cookie, a header
value or a record. itda ls --json adds each action’s verified (its last try
answered) and pass_rate, and per site the reads that fail now and then
(unstable).
itda fsck runs fixed rules over it and prints what is worth a look, worst
first, each with the command to run:
$ itda fsck board
board: 3 to look at
2 read_needs_input: get-notice needs notice_id; list-notices gives it at /items/0/id
fix: itda call board.list-notices --json
2 endpoint_untried: list-archive has never been tried
fix: itda check board
3 endpoint_untried: post-notice has never been tried
fix: itda describe board.post-notice
| Rule | Priority | When |
|---|---|---|
read_failing | 1 | a read failed every one of its last tries (up to seven) |
flapping_read | 1 | a read failed at least twice in its last seven tries and passed at least once among them |
artifact_changed, artifact_missing | 1 | spec.yaml or the last verdict changed or went away since itda wrote it down |
read_needs_input | 2 | a read needs an input and was never tried; names a list read whose answer has a field of that name (or id) and a fitting type, and its pointer. That is a guess from shapes, never checked against a value |
endpoint_untried | 2 (read), 3 (change) | an action was never tried; itda never tries a change on its own, so a change stays here until someone asks for it |
finding_without_evidence, dangling_edge | 3 | a note or a link with nothing behind it |
A call itda refused before sending (waiting for an approval, a bad input, an
expired page handle) is kept as a record but is not a try; a 404 the site
answered is one. itda fsck sends
nothing. It exits 0 when no gap is at priority 1 and 1 when one is; --json
prints status (ok or problems) and each site’s gaps with rule,
subject, priority, reason and remedy (command, plus the MCP tool
and arguments to call next). itda setup ends with the top three. itda describe <site> adds what can be read and changed, how one read feeds another,
and a few ways to ask an agent.
Draft a changed site again
A definition that itda setup, itda draft ... --install or itda record --install made remembers where it came from, in sources.json next to it:
{
"pages": ["https://board.example/board"],
"description": "https://board.example/openapi.json",
"graphql": ["https://board.example/graphql"],
"recording": "recording.har",
"lang": "en",
"drafted": {"get-board": "<sha256 of the operation as drafted>"},
"drafted_at": "2026-10-22T09:00:00.000Z"
}
pages lists addresses the draft read; description is the API description it
found (an address or the file given to itda draft openapi); graphql lists
endpoints it asked what they offer. recording is the HAR kept under the site
when itda draft har or itda record --save ... --install had one. A refresh
needs only names and shapes, so the copy keeps the recording’s shape: cookie,
token and session values, private query and body fields and sign-in call answers
become <redacted> before saving. itda remove deletes the copy with the site.
template names the site’s ready-made template (gitlab, jira…), with its
API base in base_url. A refresh fills that template again and rereads the page
if it still opens. base_url, include and lang otherwise record the draft’s
choices; itda omits any you did not give. Nothing in the file is a secret.
drafted holds each operation’s original draft hash. A refresh uses it to
distinguish hand-edited operations from those it may replace. A definition
installed with itda add has no sources and cannot be refreshed.
When a check says site_changed, draft the site again from those sources:
$ itda refresh board
Drafting 'board' again from its pages...
Looking at board.example...
As installed: site_changed (the site changed; run itda refresh board to draft it again from the same sources).
What would change:
~ get-board changed: evidence, response
~ feed changed: path, evidence
= list-notices kept as you edited it
+ search-board added: Search on Notices
Write these changes to 'board'? [y/N] y
Updated 'board'. Checking it...
board.get-board ok HTTP 200 131 ms
...
verdict ok: every read still matches the site
The merge keeps the site’s name, title and base_url, defaults, auth and
the header comments as they are, and goes through the operations by name. YAML
differing from the last source draft means hand edits. itda keeps that operation
as is. One the new draft changed (a renamed column, a moved endpoint, a response
check) takes the new version. itda drops what the sources no longer draft only
if its read also failed the check. itda tries a new operation once and adds it
if it answers. itda withdraws pending approvals for the site, as for any
replaced definition. -y writes without asking; without it, itda writes only
after you say yes. itda checks the site right after and prints the verdict. The
exit code says whether the new definition matches. Over MCP, itda_manage with
refresh does the same after you confirm the diff in a form.
Checklist before sharing a definition
itda spec validatefinds no problem.- Every read declares a
response, anditda checkpasses. - Every change whose records a read lists names that read in
confirm. - Every
descsays what the action does, in one line. evidencepoints at something a reviewer can check.- No credentials, personal data or internal hostnames in the file you share.
- The site’s terms allow automated access for your use.
Languages
itda speaks twelve languages where a person reads it: the lines itda setup
and its friends print, the landing page and the README. English is the
source. Korean was edited by hand and is kept as it is. The other ten are
translated from English with the glossary below.
| Tag | Language | CLI | Landing | README |
|---|---|---|---|---|
en | English | apps/cli/text/en.json | site/index.html itself | README.md |
ko | 한국어 | apps/cli/text/ko.json | site/i18n/ko.js | README.ko.md |
ja | 日本語 | apps/cli/text/ja.json | site/i18n/ja.js | README.ja.md |
zh-Hans | 简体中文 | apps/cli/text/zh-Hans.json | site/i18n/zh-Hans.js | README.zh-Hans.md |
zh-Hant | 繁體中文 | apps/cli/text/zh-Hant.json | site/i18n/zh-Hant.js | README.zh-Hant.md |
es | Español | apps/cli/text/es.json | site/i18n/es.js | README.es.md |
fr | Français | apps/cli/text/fr.json | site/i18n/fr.js | README.fr.md |
de | Deutsch | apps/cli/text/de.json | site/i18n/de.js | README.de.md |
pt-BR | Português (Brasil) | apps/cli/text/pt-BR.json | site/i18n/pt-BR.js | README.pt-BR.md |
vi | Tiếng Việt | apps/cli/text/vi.json | site/i18n/vi.js | README.vi.md |
id | Bahasa Indonesia | apps/cli/text/id.json | site/i18n/id.js | README.id.md |
ru | Русский | apps/cli/text/ru.json | site/i18n/ru.js | README.ru.md |
What is translated, and what is not
What a person reads. Everything an agent acts on stays English, on purpose, so that a definition, an error or a tool behaves the same in every conversation. Translated:
- the lines the CLI prints during
itda setup,itda login,itda refresh,itda fsckand the first bareitda, all throughapps/cli/src/text.rs; - what
itda describe <site>anditda ls --jsonsay a person can do with a site (user_guide) and the example prompts (natural_prompts), from one table of phrases incrates/prep/src/guide.rs;itda_sitesgives them to agents in English; - the action descriptions
itda setupanditda draftwrite, from one table of phrases incrates/prep/src/label.rs(List what {title} shows,Search on {title}). The language is the one the person set, else the page’s own<html lang>, else English. The site’s own words (titles, field and operation names) stay as the site wrote them, and so does the kind of record (news articles) outside Korean. Agents read every one of these languages, so a description in Japanese works as well as one in English; what an agent acts on (tool names, error codes, remedies) stays English; - the landing page and its title and description;
- the README.
Not translated:
- MCP tool descriptions, error codes and their
remedylines, the keys of the JSON envelope. Agents read them. - Site definitions apart from the drafted
desc: the ready-made ones and the reviewed ones inexamples/are English originals, and nothing translates a definition once it is written. - The handbook under
docs/(this file included).docs/SPEC.mdalone is 41 KB and changes with every minor release; a translation would be wrong within weeks. The README carries what a newcomer needs in every language. og:titleandog:descriptionon the landing page. Crawlers do not run scripts, so they see English.
How the language is chosen
The CLI reads ITDA_LANG, then the system LANG, and takes the first
language it supports. The tag may be BCP 47 (ja, zh-TW, pt-BR) or a POSIX
locale (ja_JP.UTF-8, zh_TW.Big5, C). Encoding and modifier are dropped,
_ reads as -, case does not matter. Then:
| Tag | Language |
|---|---|
zh-TW, zh-HK, zh-MO, zh-Hant* | zh-Hant |
any other zh* (zh, zh-CN, zh-SG, zh-Hans*) | zh-Hans |
any pt* | pt-BR |
in (the old code for Indonesian) | id |
C, POSIX, empty | en |
| anything else | its first subtag when that is a language above, else en |
ITDA_LANG=ja itda speaks Japanese whatever the system says. A key that a
language file lacks falls back to English.
The landing page picks ?lang= from the address, then the last choice on
that device (localStorage), then the first entry of navigator.languages
that maps to a language above, with the same rules. The picker in the header
changes it and remembers it. <html lang>, the title and the description
change with it.
Glossary
Each language uses these words for these things, in the CLI, on the landing page and in the README alike. A translation that needs another word for one of them changes this table in the same pull request.
| English | ko | ja | zh-Hans | zh-Hant | es | fr | de | pt-BR | vi | id | ru |
|---|---|---|---|---|---|---|---|---|---|---|---|
| site | 사이트 | サイト | 站点 | 網站 | sitio | site | Site | site | trang web | situs | сайт |
| action | 액션 | アクション | 操作 | 操作 | acción | action | Aktion | ação | hành động | aksi | действие |
| read (noun) | 읽기 | 読み取り | 读取 | 讀取 | lectura | lecture | Lesevorgang | leitura | lượt đọc | pembacaan | чтение |
| change (noun, an action that writes) | 변경 | 変更 | 更改 | 變更 | cambio | modification | Änderung | alteração | thay đổi | perubahan | изменение |
| approval / approve | 승인 / 허락 | 承認 / 承認する | 批准 / 批准 | 核准 / 核准 | aprobación / aprobar | approbation / approuver | Freigabe / freigeben | aprovação / aprovar | phê duyệt | persetujuan / menyetujui | подтверждение / подтвердить |
setup (itda setup) | 설정, 세팅 | セットアップ | 设置 | 設定 | configuración | configuration | Einrichtung | configuração | thiết lập | penyiapan | настройка |
| agent | 에이전트 | エージェント | 代理 | 代理 | agente | agent | Agent | agente | agent | agen | агент |
| definition (a site definition) | 정의 | 定義 | 定义 | 定義 | definición | définition | Definition | definição | định nghĩa | definisi | определение |
| sign in / sign-in | 로그인 | サインイン | 登录 | 登入 | iniciar sesión / inicio de sesión | se connecter / connexion | anmelden / Anmeldung | entrar / login | đăng nhập | masuk | войти / вход |
| record / recording | 녹화 | 記録 | 录制 | 錄製 | grabar / grabación | enregistrer / enregistrement | aufzeichnen / Aufzeichnung | gravar / gravação | ghi lại / bản ghi | merekam / rekaman | записать / запись |
| draft (noun and verb) | 초안 | 下書き | 草稿 | 草稿 | borrador | brouillon | Entwurf | rascunho | bản nháp | draf | черновик |
check (itda check) | 점검 | 点検 | 检查 | 檢查 | comprobación | vérification | Prüfung | verificação | kiểm tra | pemeriksaan | проверка |
refresh (itda refresh, draft again) | 다시 초안 | 再作成 | 重新起草 | 重新起草 | volver a redactar | nouveau brouillon | neu entwerfen | rascunhar de novo | soạn lại | menyusun ulang | составить заново |
| keyring | 키링 | キーリング | 密钥环 | 鑰匙圈 | llavero | trousseau | Schlüsselbund | chaveiro | keyring | keyring | связка ключей |
Product and command names stay as they are: itda, Claude Desktop, Claude
Code, Cursor, VS Code, Codex, GitHub, MCP, and every itda ... command,
flag, exit code, verdict (ok, signed_out, site_changed, blocked,
unreachable) and error code.
Rules for a translation
- A
{field}in a CLI string is filled in by the code. Every translation keeps the same set of fields as English, in whatever order its grammar wants. A test checks that. - A landing string keeps every HTML tag and attribute, every
<code>and link exactly. Only the words change. - Numbers, commands, code blocks, file names, addresses and the console transcript’s prompt line are not translated.
- The example hosts (
the-site-you-use.example,intranet.example) stay exactly as they are in every language. They are reserved example domains inside commands people copy, not prose. The transcript’s output lines are what the binary prints in that language: they come from the CLI strings. - The README keeps the English section order, the same headings count, the same links and the same images. Its first lines carry the same language line as every other README, with its own language in bold.
- Plain words, in the voice of the English text. No slogans.
Add a language
- Copy
apps/cli/text/en.jsontoapps/cli/text/<tag>.jsonand translate the values. Add the tag toLANGUAGESincrates/types/src/lang.rs, toSUPPORTEDinapps/cli/src/text.rsand the file to theinclude_str!list next to it, and toLANGSinapps/cli/tests/i18n.rs. Add a column toTEMPLATESincrates/prep/src/label.rsand incrates/prep/src/guide.rs.cargo test -p itda-cliandcargo test -p itda-prepcheck the keys and fields. - Copy
site/i18n/ko.jstosite/i18n/<tag>.js, change the tag inwindow.ITDA_I18N["<tag>"]and translate the values. Add the tag and its name in its own language to the<select data-lang-switch>insite/index.htmland toLANGSinsite/main.js. Dropflow.srcunless the diagram was redrawn in that language (python3 assets/src/flow.py). - Translate
README.mdtoREADME.<tag>.mdand add the language to the language line of every README. - Add the row to the tables above and a line to
CHANGELOG.md.
Checks
cargo test -p itda-cli text::: every language has every key ofen.jsonand no other; every key has the same{fields}in every language; locale tags resolve as the table says; the strings and the description phrases cover the same languages.cargo test -p itda-prep label::andguide::: every description, guide and prompt phrase exists in every language, with the same{fields}as English.cargo test -p itda-cli --test i18n: everysite/i18n/<tag>.jshas the keys ofindex.htmland the same HTML tags and links per key, and links to a heading its README has; every README has the headings, code blocks and links ofREADME.mdand the same language line; the picker lists every language.- A pull request that changes
README.md,site/index.htmlorapps/cli/text/en.jsonchanges the eleven other languages in the same pull request, or says in its description what it leaves for later.
Architecture
itda is a Rust workspace that builds one binary, itda. This page explains how
a call flows through it, what it keeps on disk, and why the crates are split
the way they are.
Layers
Each crate depends only on crates in a lower layer. Cargo rejects cycles. CI
runs cargo xtask check-layers to check the direction.
| Layer | Crate | Responsibility | I/O |
|---|---|---|---|
| 0 | itda-types | Error contract and exit codes, URL checks with the Public Suffix List, redaction, file helpers | files |
| 1 | itda-html | Script-free HTML and embedded-JSON extraction; what a page shows about itself (evidence) for drafting; the routes a page’s docs and scripts name (docs); the shape of a page or an answer without its values (shape) | none |
| 1 | itda-store | One SQLite connection on its own thread, with per-component migrations | SQLite |
| 2 | itda-kb | The knowledge base per site: pages and actions, how they relate, every try with its verdict, notes, file hashes. Shapes only, never a value. The gap rules and the pass rates. Nothing that sends a request reads it | SQLite (through itda-store) |
| 2 | itda-spec | Definition format, request building, response verification | none |
| 3 | itda-policy | Read/write classification, definition safety checks, single-use approvals | SQLite |
| 4 | itda-browser | Sign-in and recording; isolated read drivers, guarded browser egress, authenticated worker and bounded session pool | browser, loopback IPC |
| 5 | itda-net | Bounded HTTP and adaptive reads, contract-aware strategies, shared pacing and cooldowns, private-address guard | network, SQLite |
| 6 | itda-vault | Accounts and secrets: OS keyring, environment variables, or memory | keyring, SQLite |
| 7 | itda-runtime | Runs an action end to end; approvals for changes; audit log | files |
| 8 | itda-prep | Drafts definitions from OpenAPI documents, recordings and fetched pages, and holds an agent’s draft to the evidence (ground). Does no I/O; the CLI fetches | none |
| 9 | itda-setup | Steps for itda setup, login and check: probe, draft, try every read, keep what works; installed sites; the preparation loop an agent runs (prepare), which ends in the same install. Questions use a trait and progress uses a callback, so the CLI and MCP server share them | network, files, browser |
| 10 | itda-mcp | MCP server over stdio; with a setup context, itda_setup, itda_login, itda_check, itda_sites, itda_manage, itda_shape and itda_prepare | stdio |
| 11 | apps/cli | The itda command; the only place that picks paths and settings | all |
itda-spec never does I/O. Response checks take a ResponseLike trait, so
testing and reusing the format needs no network stack.
One call
itda call sandbox.list-notices --q 점검 and the MCP tool
sandbox.list-notices take the same path:
- Resolve. The CLI loads
specs/sandbox/spec.yaml.itda-policyvalidates it and refuses unsafe definitions with named problems. - Check inputs. Every input must be declared; itda checks types and required inputs as it builds the request.
- Classify.
operation_effectdecides read or change from the method, name, path, inputs and any GraphQL document. - Approve (changes only). The first call stores the exact request (site,
action, account, inputs, definition hash, credential fingerprint) and returns
approval_required. Afteritda approve, the next call with that id must match the digest; itda uses up the approval before sending. - Credential. The vault supplies the account’s secret. itda attaches a header credential only when the destination has the credential’s origin.
- Send.
itda-netresolves DNS and refuses private addresses unless allowed. It waits for the site’s pacing slot, then sends with size and time limits. In direct-HTTP mode every request carries theitda/<version>user agent; in adaptive mode every request, a change included, carries the installed browser’s, so the direct requests and the browser look the same to the site. When a read is answered with a challenge, or the answer lacks the fields the definition promises, itda tries it again inside the browser (a fetch, then for aGETa rendered page) and remembers which way worked. Changes never retry or follow redirects. When a change’s answer is lost and its definition names a confirming read (confirm), itda runs that read once and reportslanded,not_foundorunknownnext toaction_outcome_unknown; it never resends. - Classify the answer. A challenge the browser did not get past fails that
read, not the site. In direct-HTTP mode a block or challenge pauses the
site until
itda resume. A 401 on a signed-in request pauses that account either way. A rate limit sets a cooldown every transport respects; it is not a reason to try the browser. - Verify and shape. The declared response (JSON pointers, HTML fields)
must match and the data must extract, or the call fails with
response_mismatch; in adaptive mode a mismatch is first a reason to try the next way, and only an answer that passed is kept and its way remembered. An error the site itself reports (a GraphQLerrorslist) ends the call and is not retried. itda then projects the data through a model and removes the secret. - Record. The audit log gets a record; a change got one before step 6. The knowledge base gets one line: the verdict, the status, the time it took and the keys and types of the answer, never an input or a value.
- Return. The CLI prints the data or JSON envelope. MCP marks it
untrusted, cuts it to 24 KB and returns it as
structuredContent.
What is stored
Definitions and application state are in the data directory (ITDA_HOME or the
platform’s data folder). itda doctor prints it. Windows browser workers use
the user’s local application-data directory instead.
| Path | Contents |
|---|---|
specs/<site>/spec.yaml | Installed definitions |
itda.db | Pacing, pauses, transport strategies, approvals, account names and which secrets exist (vault_account, vault_secret). No secret values. |
itda.db, kb_* tables | The knowledge base: pages and actions (kb_node) with their shapes (selectors and counts, field names, keys and types), how they relate (kb_edge), the last 200 tries of each action with stage, verdict, HTTP status and time (kb_attempt), notes with a full-text index (kb_finding), the hashes of the definition and the last verdict (kb_artifact), and the gaps the rules found (kb_gap). No answer, input, cookie, header value, record or form value. itda remove deletes a site’s rows. |
audit.jsonl | Hash-chained audit records: names, outcomes, input digests. No input values. |
OS keyring, service itda | Secret values, one entry per site, account and name |
browser-runtime/<namespace>/ | The browser worker’s secret, its lock files, the installed browser’s user agent, and the temporary browser profiles while they are in use. One directory per itda build; a directory of a build that is gone is removed once nothing has touched it for a day and no worker holds its lock. On Windows this lives under the user’s local application data. |
Browser worker
A read that needs the browser goes to a worker process; direct HTTP needs no worker, and the first read that needs one starts it. One worker belongs to one data directory, itda executable, browser and private-address setting. It keeps up to four browser sessions, one per site and account, closing the least recently used when a fifth is needed, and exits after five idle minutes. Reads for the same site and account queue behind each other inside the worker; the turn-taking between processes and the pacing per site stay in SQLite, as for direct HTTP. A changed credential gives the session a new generation, so the old cookies are not reused; a changed query does not.
The CLI and the worker talk over the loopback address in itda’s own message format, not HTTP. Before any credential is sent, both sides prove they hold a secret kept in a file only this user can read: each signs the other’s fresh nonce together with the generation, protocol version, direction and the hash of the itda executable. Messages carry a bounded JSON header and the raw bytes. A reset ends every queued generation; shutdown waits for the reads in flight and gives the ownership lock back. Lock files are never deleted to take over.
Everything the browser loads goes through itda’s own SOCKS proxy, which
resolves each name and connects only to addresses the private-address rule
allows. Frames and workers the page creates are paused and set up the same way
before they run. Other sites get neither the account’s credential headers nor
its referrer. A resource POST passes only when the policy crate reads it as
a GraphQL query. A Retry-After from one resource cools that resource’s site
down in the shared gate without failing the page that loaded it.
Each browser is started through a small supervisor process whose standard input the worker holds. When that pipe closes, the supervisor kills the browser, waits for it and removes its private profile, even before the browser has written its DevTools port. Profiles the worker registered are also cleaned up at the next start, once the browser shows (through its command line) that the profile is its own. While a read’s outcome is unknown the account’s turn is kept; once the worker is known to have died, the read gets one more try.
Decisions
- Definitions over browsing. Browser agents repeat the same work on every task (finding the button, reading the page). Here it happens once, when the definition is written and reviewed. Calls are cheap, typed and checkable.
- Changes need a person. No code path sends a change without the user’s approval for that exact request. No MCP tool grants approval.
- Adaptive transport. Bounded browser recovery is allowed for reads, with account isolation, shared pacing and no write replay. See HONEST_CLIENT.md.
- Website text is data. itda marks results untrusted for agents; an agent gets instructions only from itda itself.
- Local first. No server, account or telemetry. Secrets stay in the OS keyring; a browser session a read recovers in lives in a private profile on this computer and is removed when it closes.
- The knowledge base informs; it does not decide. It tells a person or an agent what was tried, what failed and what needs an input. No transport, header, retry or account choice reads it: the runtime gets a handle that can only write a try, and a test makes the same requests with it empty and full.
Roadmap
| Next | What it adds |
|---|---|
| Desktop approvals | Approve through a desktop prompt instead of a terminal or MCP elicitation |
| Signed site packs | Share and install reviewed definitions with hash-locked, signed packs |
Provenance
Parts of the foundation were ported from Hycli (Apache-2.0). Each ported file says so in its header; NOTICE lists them. The main changes from the original:
| Here | Hycli source | Change |
|---|---|---|
types/error.rs | src/apperr.rs | Product names removed from remedies; SQLite conversion behind a feature |
types/url.rs | URL helpers in src/net.rs, src/runtime.rs | Same-site checks use the Public Suffix List instead of a fixed list |
html | parts of src/html.rs | Only what running and verifying need |
spec | src/spec.rs | Effect moved here; ResponseLike decouples it from the network |
policy | src/policy.rs, src/session_recipe.rs | Logic kept; pacing moved to net. Approvals must be granted before use |
net | src/net.rs, src/defend.rs | Data-contract-validated recovery, persistent route learning and shared cooldowns; an exhausted adaptive read does not pause the site |
Transport policy
itda acts for a person, on websites that did not build it an API, with that person’s own account. It reads a site over plain HTTP and, when the site answers a read with a challenge page, tries that read once more inside the browser installed on the computer. Moving to the browser is the only thing a challenge changes: it does not let itda send a change, carry a credential elsewhere or skip a wait the site asked for.
How a read recovers
Direct HTTP goes first. When the answer is a challenge page, or does not have
the fields the definition promises, itda runs the same read inside a hidden
window of the installed browser: a fetch from the page’s own origin, then, for
a GET, the rendered page. An answer counts only when its response check
passes and the data extracts. An error the site itself reports, such as a
GraphQL errors list, ends the read instead of moving it on.
itda remembers which way worked for an hour, per site, account and action, so the next call starts there instead of paying for the failed HTTP try again; changing an input does not forget it. A way that failed is skipped for a few minutes (five for direct HTTP, three for the browser). This cache holds no secrets.
The browser is the one installed. itda downloads neither a browser nor a captcha solver, and does not solve captchas. Every request in this mode, a change and a sign-in form included, carries that browser’s user agent, over HTTP too, and the hidden window keeps the browser’s own client hints. The site sees one client whichever way a request goes, so a session or a challenge clearance the browser earned also holds for the direct requests. itda does not copy the browser’s TLS fingerprint, and nothing here guarantees that a challenge passes.
Each site and account gets its own browser session, isolated from every other.
A worker process starts on first need, keeps up to four sessions warm across
CLI calls and exits after five idle minutes; no daemon command is needed. Reads
for different sites or accounts run at the same time, under the shared pacing;
two reads for the same one take turns. Signing in again, signing out or
removing the site closes its session; changing a query, body or Accept
header does not.
Everything the page loads goes through itda’s own proxy, so the address checks of a direct request apply to every resource, frame and worker: private addresses are refused, other sites get neither the account’s credentials nor the page’s address as referrer, and only reads pass, plus a GraphQL query itda has read and found to be one (no mutation, subscription, batch, or document it cannot tell). A browser answer is capped at 16 MiB, and the read’s own byte and time limits still apply. After each read the page and its workers are stopped and the proxy drained; the session stays warm.
A 429 or Retry-After from one resource the page loads cools down that
resource’s site, across CLI calls, without throwing away the page’s data when
the resource was optional. A read that cannot be satisfied during that wait
fails with retry_after, upstream_origin and paused: false.
If the worker dies mid-read, itda notices (its ownership lock, or a new worker answering) and tries that read once more within the original deadline. A small supervisor process closes a browser whose worker died, even before the browser has reported its DevTools port. While itda does not know whether a read ended, it keeps the account’s turn rather than start a second read next to it.
ITDA_TRANSPORT=http keeps every request on direct HTTP with the itda/<version>
user agent; a block or challenge then pauses the site until itda resume, as
before. ITDA_TRANSPORT=auto, or leaving it unset, is the behaviour above.
Boundaries
| Rule | Enforcement |
|---|---|
| Only a read may move to the browser on its own. A change goes once, over direct HTTP, after a single-use approval bound to its exact inputs, with no retry and no redirect. | itda-net read recovery; itda-policy classification and Approvals; itda-runtime write path |
In direct-HTTP mode every request identifies itself as itda/<version>. In adaptive mode every request carries the installed browser’s user agent instead, so the direct requests and the browser look the same to the site. A definition cannot set sec-* headers; the transport owns them. | itda-net default headers; itda-policy refuses sec-* headers in definitions |
| Credentials go only to the site and account they belong to. A redirect never carries them elsewhere, and a page in the browser cannot send them, or its address as referrer, to another origin. | itda-policy validate_spec; itda-net redirect checks; itda-browser request interception |
| Private addresses need explicit permission, for a direct request and for everything the browser loads. | itda-types::url; itda-net resolve; itda-browser proxy |
itda processes on the same data directory share one limit for discovery and actions: one request at a time per site, about 1.2 seconds apart, whichever way the request goes. A definition may ask for more with defaults.interval. A read waits out a short Retry-After once; a longer one is reported with the time the site asked for. | itda-net Gate |
The gate re-reads the request it is about to send, not only the declared inputs: the rendered address and body, and any GraphQL document in them. A declared read that turns out to change the site goes to the user for approval with the reason; one whose effect cannot be told is not sent (effect_unknown). A redirect or next-page address carrying a GraphQL mutation is not followed. Headers that rewrite the method or the address (X-HTTP-Method-Override, X-Original-URL, X-Rewrite-URL, …) are refused. | itda-policy rendered_request_effect, safe_read_url, overrides_method; itda-runtime |
A signed-in request answered with 401 pauses that account (auth_required); it does not move to the browser. | itda-net Gate |
A read the browser did not get past fails with blocked_by_site and paused: false. Nothing is paused; only that way of fetching is skipped for a few minutes. In direct-HTTP mode (ITDA_TRANSPORT=http) a block or challenge pauses the whole site until the user resumes it. | itda-net |
| itda does not solve captchas and never downloads a browser; it uses the one installed. | itda-browser |
| itda records every call, approval and change in a hash-chained log on the user’s device, and hands website text to agents as untrusted data. | itda-runtime audit; itda-mcp |
| The knowledge base does not choose how a request is sent: no transport, header, retry or account choice reads it. | itda-kb Recorder (write only); itda-runtime |
| The window itda opens for sign-in, recording or watching a page load is the person’s own. itda opens the named page and watches, scrolling to the bottom once when watching a load by itself, but never clicking, typing or navigating. The window a read recovers in is hidden and never shown. | itda-browser SignIn, Recorder, transport |
| Sign-in through a site’s own form belongs to the person: they type the user name and password. itda sends the form once, with the same user agent as its other requests, to its own action on the same site, with the page’s fields. itda reports a refused sign-in without retrying or pausing anything. itda never keeps the password; an agent cannot start this path. | itda-setup login::form |
In itda setup, a read the site still refuses is dropped from the draft with a
line saying so, and nothing more is sent to the site in that run. Recovery
never changes an action’s inputs, whether it counts as a read or a change, or
what it needs approving. An agent’s preparation loop (itda_prepare) checks
only reads, through the same path a call takes, and counts each request,
recovery included; its advice never names a way around a refusal: a block
means pause and tell the person.
A definition may set a different User-Agent in defaults.headers when a site
asks for one; that choice is visible in the spec and in review. It cannot set
sec-* headers: the transport owns those.
After a pause
A site pauses only in direct-HTTP mode, or for a sign-in the site no longer
accepts. Check the site in your browser, then run itda resume <site>: that
clears the site’s pauses and the ways of fetching it remembered, and closes the
browser session for that site and account. Sign in again only when the
account’s session was rejected (auth_required), not when an API refused one
request (api_denied). Adaptive mode clears an old challenge pause for the
site and account on its own; it never clears a sign-in pause or a rate-limit
cooldown.
Transport measurements
Measured on October 23, 2026 on Apple M4 Pro, macOS arm64, with Google Chrome 155.0.8059.86. Both CLIs used release builds and identical synthetic loopback responses. This measures the CLI, not just the browser driver.
Results
Each primary cohort has 20 warm samples. Times are process-spawn to process-exit, including internal pacing. Cold samples are separate.
| Cohort | Hycli p50 / p95 (ms) | itda p50 / p95 (ms) |
|---|---|---|
| Direct HTTP | 20.6 / 48.0 | 19.7 / 56.1 |
| Browser fetch, default lifecycle | 2,240.4 / 3,845.8 | 115.1 / 167.5 |
| Render, default lifecycle | 3,071.2 / 5,281.2 | 115.5 / 210.3 |
| Render, Hycli attached to an already-running isolated Chrome | 1,559.0 / 1,571.7 | Same itda warm render cohort above |
| Four origins, concurrent render calls | 6,434.3 / 7,024.1 | 1,302.7 / 1,455.4 |
For the concurrent batch, both tools completed 20 reads with four workers: Hycli took 32.251 seconds (0.620 reads/s); itda took 6.471 seconds (3.091 reads/s). Pacing remains enabled.
The primary run completed 195/195 reads, including cold samples, with no cleanup failures. With a warm worker, itda’s browser-fetch p95 was 23.0x lower than Hycli’s and its render p95 25.1x lower; concurrent throughput was 4.98x higher. Most of that gap is lifecycle: a default Hycli browser call starts and closes Chrome, itda keeps its worker. Against Hycli attached to an already-running Chrome, itda’s render p95 was 7.47x lower.
The warm numbers hold only while the worker is alive; it exits after five idle minutes. A caller that comes back later pays the cold cost: the first direct calls took 248 ms for Hycli and 918 ms for itda; the first browser fetch 2,966 ms and 2,792 ms; the first render 3,199 ms and 3,143 ms. These are fresh data-directory and profile samples, not machine-cache-flushed experiments.
The aim for direct HTTP was a p95 within 5% of Hycli’s. It was not met. One predefined follow-up alternated AB/BA order for 30 pairs, without changing binaries or resetting policy per call:
| Direct HTTP follow-up | Hycli | itda |
|---|---|---|
| p50 | 18.6 ms | 17.7 ms |
| p95 | 68.1 ms | 94.2 ms |
| p99 | 118.4 ms | 163.7 ms |
All 62/62 follow-up reads, including cold samples, and cleanup succeeded. itda’s median was slightly lower, but its p95 was 16.9% higher in the primary run and 38.4% higher in the paired run. With 20 and 30 samples, p95 and p99 are the largest one or two values, so those percentages are rough; the medians are the stable figure. These comparisons do not identify the cause or show a regression from itda’s pre-change version, which was not benchmarked here.
Method
- Hycli (the project itda’s
netlayer derives from; see the README’s credits) is the unmodified official checkout at192d71329720f553b382a366573ef06f4e156bb7. itda is thefeature/adaptive-transportbranch as it was on the day; the data files carry the binary’s SHA-256. - Each cohort has its own persistent data directory. Installed definitions, pacing, budgets and browser state are not reset between samples.
- Interactive primary samples have equal 1,500 ms think time outside the measured interval. Paired samples alternate tool order with 1,500 ms between pairs. Concurrent samples include all pacing waits.
- Direct mode is explicit HTTP for both. itda recovery is automatic; Hycli uses its explicit browser transports because its localhost policy suppresses automatic escalation. Default Hycli browser calls launch/close the engine; itda retains its worker. The separate live-CDP control exposes that lifecycle difference rather than hiding it.
- Payload fields are checked, not just exit codes. Only correct successes enter latency quantiles; a failed attempt would still be counted. Quantiles use the nearest rank. No builds or browser QA ran during measurement.
- The script needs Bun (
Bun.spawn,Bun.serve) and a locally built Hycli; it takes its flags as--name valuepairs and ignores a flag it does not know. - A CLI read is not necessarily one HTTP request. For 20 warm browser-fetch reads, the fixtures observed 60 total requests from Hycli and 40 from itda; warm render observed 40 and 20. Bootstrap/favicon requests are included.
- Results are local synthetic measurements, not a real-site success-rate study. Windows/Linux behavior and arbitrary website challenges were not exercised.
The full raw samples, cold results, counts and binary SHA-256 hashes are in the primary data and the paired data. Only local binary paths were replaced with labels; measurements are unchanged.
Reproduce
Build the pinned Hycli checkout using its build instructions, including its
dashboard assets (npm --prefix dashboard ci, npm --prefix dashboard run check,
npm --prefix dashboard run build) before cargo build --release --locked --bin hycli.
Set HYCLI_BIN to that executable and CHROME to the installed browser.
From this worktree:
cargo build --release --locked -p itda-cli
ITDA_BIN="${CARGO_TARGET_DIR:-target}/release/itda"
bun scripts/bench-transports.mjs \
--itda "$ITDA_BIN" --hycli "$HYCLI_BIN" --browser "$CHROME" \
--out /tmp/itda-transports.json --samples 20 --think-ms 1500 --live yes
bun scripts/bench-transports.mjs \
--itda "$ITDA_BIN" --hycli "$HYCLI_BIN" --browser "$CHROME" \
--out /tmp/itda-direct-paired.json --samples 30 --think-ms 1500 \
--only direct-paired
Verification
With ITDA_TEST_BROWSER set to the installed Chrome, workspace tests,
cargo fmt --all --check, strict workspace/all-target clippy, layer and
changelog checks, and the locked release build passed. Actual CLI tests cover
cross-process session reuse, concurrent origins, worker death after execution
and worker death before DevTools registration. Browser tests cover credential
changes, raw A-B-A redirect cookies, foreign resources/frames/workers, read-only
GraphQL, optional-resource cooldowns and a shutdown that leaves nothing
running.
Threat model
itda runs on the user’s machine and acts on websites for an AI agent. itda treats the agent, the SiteSpec and every website response as untrusted. Only the user can approve a change or resume a paused site.
“Today” lists controls that exist in the code now. “Planned” lists controls from the roadmap that are not built yet.
Assets
- Website credentials and session cookies
- The user’s accounts on those websites (what a change can do to them)
- The user’s local network (what a request can reach)
- The record of what was done (audit trail)
- Files on disk: attachments itda saved, and files the user sends
The knowledge base in itda.db adds no asset: it holds the shape of pages and
answers (selectors, counts, field names, keys and types), verdicts, status
codes and times, never an answer, input, cookie, header value, record or form
value, and nothing that sends a request reads it.
Threats and controls
| Threat | Path | Today | Planned |
|---|---|---|---|
| Agent runs changes the user did not want | Agent calls a write action, or repeats one | Reads are the default. An action is a read only if method, name, path, inputs and GraphQL agree. itda reads the request it built once more before sending it (rendered address and body, GraphQL documents in them): a declared read that changes the site goes to approval with the reason, one whose effect cannot be told is not sent. itda classifies reads again after preflight with the found values filled in and refuses headers and inputs that override the method or rewrite the address. A redirect or next-page address carrying a GraphQL mutation is not followed. Changes need a single-use approval bound to the exact request, valid five minutes. Only itda approve in a terminal (no piped input) or a form the MCP client shows the person grants it. Review shows the action’s description and every input. An unanswered approval stays (approval_pending); other inputs withdraw it. Logout, remove, a new login or a replaced definition withdraw every approval for the site. No MCP tool approves. itda uses up approval before sending and never retries writes. A GraphQL answer with errors behind a 2xx is an unknown outcome, never a success. After a lost answer itda may run the read the change names (confirm) to report whether it landed; that is a read, and the change is not sent again. | |
| Prompt injection | Instructions hidden in a website response | itda checks responses against the declared expectation. It marks CLI JSON and MCP results untrusted with a data-not-instructions note and cuts MCP results to 24 KB. | Output schemas limit fields and length |
| Credential leak | Credentials reaching the agent, logs or a model | itda redacts secrets in JSON and text and refuses credential-named inputs and secret-looking defaults. A refusal carries the site’s status and one of a short list of reasons, never its words. Short secrets live in the OS keyring; long ones (browser sessions) live in vault.sealed, encrypted with AES-256-GCM and a per-directory key kept in the keyring, with no plaintext fallback. itda sends a password typed for a site’s own sign-in form once to that form, on the same site, and never keeps or prints it. Only a person in a terminal can start that sign-in, never an MCP tool. itda keeps a URL session (value_from: query:<key>) in the keyring like any other and redacts it from results, error text and the audit log. Unlike a cookie, it travels in the request address and can reach the site’s server logs and any proxy in between; itda cannot redact those logs. A session a single-page app keeps in the browser (value_from: storage:<key>) is read once, over the DevTools protocol, from the window you signed in to: only that origin’s localStorage and sessionStorage, never another site’s, never a browser profile on disk, and itda never writes anything into a page. The value is kept in the keyring, sent as the header the site’s own scripts use, and redacted from results, errors and the audit log like a cookie. | The agent sees only account ids; the vault passes secrets straight to the network layer. itda redacts captures before any model sees them. |
| Another user on the machine reads the local database | A loose umask or a data directory that was made readable | The data directory is private to the user (0700 on Unix) and so is the SQLite file with its write-ahead log and shared-memory files (0600 on Unix, set when the store opens, so files left by an earlier run are tightened too). On Windows the directory’s ACL covers them. | |
| An agent preparing a site learns what the site holds, or reaches past it | itda_prepare reads pages and documents for an agent that drafts the definition | The agent gets shapes only: titles, headings of a page’s own sections, paths, methods, field and control names, selectors, keys, types, pointers and counts, never a record, a form value, a cookie, a header or a page’s text, and values an answer holds stay in the session for $result inputs. Reads go by GET without an account, only to the site’s registrable domain and the hosts the site itself linked as documentation; other sites and private addresses are refused before anything is sent. Checks are reads through the runtime’s path, read once more as rendered, at most 24 requests a session counted from the pacing record; a change is written into the definition and never sent. Notes in the knowledge base carry routes, paths, counts and verdicts, never a value. No step approves, signs in or installs past the setup path’s own questions. | |
| Credential sent to the wrong site | Malicious spec or redirect | itda binds header credentials to one origin, session credentials to one site (Public Suffix List); follows same-origin redirects only for reads | |
| Local network probing | A public spec targets private addresses | itda refuses private and local addresses unless explicitly allowed; pins DNS results per request | Per-host allow list for intranet use |
| Malicious or altered spec pack | Registry supply chain | itda refuses unsafe specs before install and names the problems | Signed packs, hash-locked installs, re-consent when a pack asks for more |
| Another process talks to the browser worker | A web page or another program on the computer finds the worker’s port and asks it for a read with the account’s credentials | The worker listens on the loopback address only and speaks its own message format, not HTTP. Before any credential is sent, both sides prove they hold the secret kept in a file only this user can read, signing a fresh nonce from each side together with the generation, protocol version and the itda executable’s hash. Messages are bounded in size. | |
| Audit tampering | Hiding what was done | The audit log is append-only and hash-chained. itda audit --verify names the first changed record. itda logs changes before sending. If a process stops before recording an outcome, itda audit and itda doctor show the change as unknown. itda reports a change that never left the machine as not sent. | Signed checkpoints kept outside the device |
| A site answers a file read with something else | A sign-in page, an error page or a part of the file in place of the attachment | itda streams a file read to a temporary name and seals it only when the status, media type, size and completeness match the definition; an HTML answer counts only when the site marks it an attachment. Nothing is kept from a failed answer. The name comes from the site’s Content-Disposition or the address, reduced to plain characters, so it never leaves files/<site>/ (or --out) or replaces a file there. The manifest carries the file’s size and SHA-256; the agent gets the manifest and a link, never the bytes. | |
| A file other than the one approved is sent | An agent swaps the file after approval, or the file changes while it is being sent | The approval names the file by name, size and SHA-256 and is bound to that hash; other bytes under the same path withdraw it. itda streams the file from one open handle, checks size and modification time before each chunk and the hash at the end, and aborts the send on a mismatch (action_outcome_unknown, reason: file_changed). The audit log records the hash. A body that carries a file is always a change, and goes only to the site’s own origin. | |
| Breaking a site’s rules | Automating a site that refuses it, or hitting it with many requests at once | All itda processes sharing a data directory send one request at a time per site, about 1.2 seconds apart, whichever way the request goes; definitions can increase this gap, not reduce it. Reads wait out a short Retry-After once; a longer one stops the call. A read answered with a challenge is tried again inside the browser, once each way, then fails without another try, and a way that failed is skipped for a few minutes. itda does not solve captchas. In direct-HTTP mode a block or challenge pauses the site until the user resumes it. | Pack metadata for terms and robots.txt |
| A page escapes the browser | A script, frame or worker in the rendered page sends the account’s cookies elsewhere, or reaches the local network | Every connection the browser makes goes through itda’s own proxy, which resolves the name and refuses private addresses the way a direct request does. Other sites get neither the account’s credentials nor the page’s address as referrer. Only reads pass, plus a GraphQL query itda has read and found to be one. The page and its workers are stopped and the proxy drained before the session is handed back. | |
| A browser left behind | The CLI or the worker exits while a browser is starting or working | Each browser is started through a small supervisor process. When the worker dies, the supervisor kills the browser and removes its profile, even before the browser has reported its DevTools port. The account’s turn is held until itda knows the read ended, so no second read overlaps it. The worker exits after five idle minutes. |
Out of scope
- A compromised user account or operating system. itda cannot protect secrets from software already running as the user.
- An agent with unrestricted shell access as the user. It could edit the data directory or drive a terminal itself. The terminal prompt keeps honest agents from approving their own changes; it is not a sandbox.
- Websites’ own vulnerabilities.
See HONEST_CLIENT.md for the rules on how itda treats websites, and SECURITY.md to report a problem.
Changelog
All notable changes to itda are listed here. The format follows Keep a Changelog and versions follow Semantic Versioning. Until 1.0, a minor version may change the definition format or the CLI.
Unreleased
0.7.0 - 2026-10-24
Reads that get past a challenge, a knowledge base per site, and an agent that
prepares a site itself. A read a site answers with a challenge page now
recovers in the browser installed on the computer, and every request in that
mode carries that browser’s user agent; a change is still sent once, over
direct HTTP, after a single-use approval. itda writes down what it found and
tried for each site, itda fsck lists what was never tried or keeps failing,
and itda_shape shows the shape of a page or an answer without its values.
When setup keeps one read or none, itda_prepare lets the agent draft the
site from its docs, seeing shapes and evidence, never what the site holds.
Added
itda_prepare, one MCP tool for an agent to prepare a site thatitda_setupdrafted thinly, as a loop the agent runs:start,recon(the entry page, the API description it links, its GraphQL schema, the routes its docs name and a draft itda made, each with an evidence id),read,search,propose,verify,reviewandinstall. itda calls no model and the agent never sees a value the site holds: only titles, paths, methods, field names, selectors, keys, types, pointers and counts. Each proposed operation is kept only when the evidence carries it (a documented or answering server, the route in the docs or a page at that path, something to check in an HTML answer), a declared read whose request asks for a change becomes a change, a route that needs a sign-in itda cannot use is dropped, and a draft that only reads the home page is refused.verifytries the kept reads through the same path every call takes, fills inputs from earlier answers with$result:<read>:<pointer>(scalars only), addsrequired_pointersfrom the field names at the top of the answer, says in counts where a page read’s selectors miss, and never sends a change. Reads go only to the site’s registrable domain and the hosts it links as documentation. A session has 120 calls, 48 page requests and 24 check requests (browser recovery counted), 6 reads perverifyand two coverage and two repair rounds.installtakes the same path asitda_setup. Whenitda_setupkeeps one read or none, its result carries aremedythat namesitda_prepare.reconwithbundles: truealso reads the scripts the entry page loads and keeps a route string only when one GET answers with data.skills/itda-prepare/SKILL.mdis the loop for skill loaders.- Reads that a site answers with a challenge page can recover through the
browser installed on the computer. itda fetches the page inside a hidden
browser window kept for that one site and account and, for a
GET, lets the page render. Every request in this mode, a change or a sign-in form included, carries the browser’s user agent, and the hidden window keeps the browser’s own client hints, so the site sees one client whichever way a request goes and a clearance the browser earned holds for the direct requests too. itda remembers which way worked for an hour, per site, account and action, so the next call does not pay for the failed try again, and forgets a way that failed after a few minutes. The browser session stays warm in a worker process across CLI calls and closes after five idle minutes; changing an input does not restart it. Everything the page loads passes the same address and read checks as a direct request: other sites get neither the account’s credentials nor a referrer, private addresses stay refused, and a page may send a GraphQL query, never a mutation. A challenge the browser did not get past fails that one read (blocked_by_site,paused: false) without pausing the site. A browser answer may be up to 16 MiB. The worker keeps its files underbrowser-runtime/in the data directory, one directory per itda build; a build’s directory is removed a day after it stopped being used.ITDA_TRANSPORT=httpkeeps direct HTTP only, with the pauses it always had.docs/HONEST_CLIENT.mdhas the rules,docs/TRANSPORT_BENCHMARKS.mdthe measurements. - A local knowledge base per site, in
itda.db. Setup, check, refresh and every call write down what was found and tried: the pages and actions, which list read can supply which detail read’s input, every try with its stage, verdict, HTTP status and time, and the hashes of the definition and the last verdict. It keeps shapes only (selectors with match counts, field names, keys and types), never an answer, input, cookie, header value or record. Nothing that sends a request reads it, anditda removedeletes a site’s rows. A call itda refused before sending (waiting for an approval, a bad input) is not counted as a try. itda fsck [site]runs fixed rules over the knowledge base and lists, worst first, the reads never tried, the reads that need an input (with the list read and pointer that can supply it), the reads that fail every recent try or now and then, and a definition or verdict file changed under itda, each with the command to run. It sends nothing; exit 0, or 1 when a gap is at priority 1. Over MCP,itda_managetakesfsck, and each gap carries the next tool to call with its arguments.itda setupends with the top three.itda_shape, a read-only MCP tool: the shape of a page (selectors with counts, named fields, the keys and types of embedded data) or of an action’s last answer (keys, types and pointers), from the knowledge base or, withfresh, read now the way setup reads a page. At most 24 KB and no values.itda ls --jsonanditda_sitessay per site how many actions answered on their last try (verified), the recentpass_rateand the reads that fail now and then (unstable).itda ls --jsonanditda_siteswith a site add auser_guide(what can be read, what can be changed, how one read feeds another) and two to fournatural_prompts, made by rule in the person’s language;itda describe <site>prints both.
Changed
-
The knowledge base records a preparation loop’s tries under a stage of their own,
prepare, and its notes under thepreparescope; the pass rate, the last success and theitda fsckrules leave both out. -
The honest-client rule changes: itda no longer stops at a challenge. Every request carries the installed browser’s user agent, a read may run inside that browser after a challenge, and a challenged read no longer pauses the site. What stays: a change is sent once, over direct HTTP, after a single-use approval; credentials go only to their own site and account; one request at a time per site, with
Retry-Afterrespected, whichever way the request goes; captchas are not solved.
Fixed
- Opening
itda.dbcould fail withinternal(“Local file operation failed”) when another itda process, the browser worker or the MCP server, closed its connection at that moment: SQLite removeditda.db-walwhile itda was making it private. A file that is gone is now left alone.
0.6.0 - 2026-10-23
Files in and out, a check after a lost answer, sessions kept in browser
storage, a gate that reads the request it is about to send, and twelve
languages. A change can name the read that shows it landed, so after a lost
answer itda checks once before anyone is asked to approve again. Reads can save
an attachment, and a change can send a file whose hash you approved. A
single-page app that keeps its session in localStorage now signs in. Setup,
the landing page and the READMEs speak twelve languages, and the mark is a
cursor striking a site.
Added
-
Twelve languages. What
itda setup,itda login,itda refreshand the first bareitdaprint now comes in English, Korean, Japanese, Simplified and Traditional Chinese, Spanish, French, German, Brazilian Portuguese, Vietnamese, Indonesian and Russian, chosen fromITDA_LANGor the systemLANG(ja_JP.UTF-8,zh-TW,ptall resolve). The landing page picks the browser’s language, has a picker in the header and remembers the choice; the README has one translation per language, linked from a language line at the top of each. What agents act on (tool descriptions, error codes and remedies) stays English. The strings live inapps/cli/text/<tag>.json,site/i18n/<tag>.jsandREADME.<tag>.md;docs/i18n.mdhas the glossary and the rules. -
Outcome check after a lost answer: a change may name the read that shows it landed (
confirm: {read, match: {pointer, from}}in the definition). When the change is sent and its answer is lost (action_outcome_unknown, exit 22), itda runs that read once, as a read, and reportslanded,not_foundorunknownnext to the error, asoutcome_checkin the CLI output,--json, the MCP result and the audit record (outcome_check,confirm_read). The change is never resent. Afterlanded, a new approval request for the same inputs carries anotein the review andprevious_outcome: landedin the answer, so the person can decline.itda auditlists lost answers with what the read said;itda_manage auditreturns them underunknown_outcomewithcheck.itda lsanditda doctorreport aconfirmthat names a missing operation, a change or a read that needs inputs as a definition problem.itda setupanditda draftfillconfirmwhen aPOST’s list read is on the same path prefix and a string input of the change appears in the read’s records; the evidence says so. The lab getslossy(8089), the scripted portal whose post is saved and then the line drops, and every reviewed lab definition names its confirming read. -
Files out: a read may declare
response: {format: file, media_types: [...], max_bytes: N, allow_empty: false}. itda streams the answer to a file underfiles/<site>/in the data directory (oritda call ... --out <dir>), seals it at the end, and returns a manifest{path, bytes, media_type, sha256}, never the bytes; over MCP the result adds aresource_linkto the file and the tool’s description says what comes back. The content type must be one ofmedia_types(an HTML answer only as an attachment), and a 206 orContent-Range, a body shorter than itsContent-Length, an empty body or one over the cap fail by name (wrong_media_type,partial_answer,empty_answer,too_large) and leave nothing on disk. File names come from the site’sContent-Dispositionor the address, reduced to plain characters, and never replace a file already there.itda checkskips file reads. A transfer longer than the gate’s turn renews it (Lease::renew) every 30 seconds; a lost turn stops the transfer (request_lease_lost). -
Files in: a change may declare
body: {multipart: {fields: {...}, file: {name, from}}}. The agent gives the path of a file on this computer in thefrominput; the approval shows the file’s name, size and SHA-256 and is bound to that hash, so other bytes under the same path withdraw it (approval_expired,reason: file_changed). The file is streamed from one open handle in 64 KiB chunks withContent-Lengthset, its size and modification time checked before every chunk and its hash at the end; a file that changes mid-send aborts the send (action_outcome_unknown,reason: file_changed). The audit log records the hash. A body with a file is always a change and goes only to the site’s own origin. -
Drafts: a page form with
enctype=multipart/form-dataand a file control becomes such a change (Upload a file to <page>), and links the page labels as attachments (adownloadattribute, download or attachment words in English or Korean, or an icon named for one) become file reads grouped by shape (download-filewith afile_idinput), withmedia_typesfrom the kinds the links name or the usual document types.itda draft haranditda recorddraft a recorded multipart post from its part names and a recordedContent-Disposition: attachmentanswer as a file read.itda describesays what a file read returns and where it lands, and which input a change sends as a file. -
Storage sessions: a site that keeps no session cookie and instead holds a token in the browser’s
localStorage(orsessionStorage) and sends it asAuthorization: Beareror a custom header now signs in and works. When you set the site up or record it, itda reads that one origin’s storage once, over the DevTools protocol, from the window you signed in to, and recognises the token as the session when the recorded calls actually sent it as a header: a JWT shape, or a long opaque value under a key named liketoken,access_token,id_token,auth,sessionorjwt. A stored value no request sent is reported and never kept. It is drafted as akind: headerauth entry withvalue_from: storage:<key>; the token lives in the keyring, never in the definition, anditda login <site> --browserreads it again at the next sign-in. If the token is a JWT withexp,itda checkreportssigned_out(notsite_changed) once it has passed, anditda_sitesshows when the sign-in expires; itda never signs in again on its own. The token is redacted from results, errors and the audit log like a cookie.value_fromon aheaderentry now acceptsstorage:<key>besidestore:<site>/<key>, and on abrowserentryquery:<key>besidecookie:<name>, with a validation error for anything else. itda reads only the signed-in origin’s storage, never another site’s, never a profile on disk, and never writes anything into a page; seedocs/THREAT_MODEL.md. The definition format is unchanged:spec_versionstays 1. -
The API description a page points at is also found in the first page’s HTTP
Linkheader (RFC 8288) and under two more relations:api-catalog(RFC 9727) anddescribedby, in the header or on<link>/<a>. Learning where costs no request.service-descwins, thenapi-catalog, thendescribedby. An API catalog is a linkset (RFC 9264): itda follows itsservice-desclinks, up to three, and takes the first OpenAPI document. Adescribedbydocument counts only when it is one; otherwise/openapi.jsonis still tried.relis matched as a list of relation types, case-insensitively, sorel="not-service-desc"no longer counts. Pagination’sheader:linkreads the header with the same parser, so a target with a comma in it orrel="Next"is read right. -
itda spec validate <file> [--json]checks a definition the wayitda addwould (Spec::validateand the policy checks), without installing it. A valid file exits 0; otherwise every problem is printed asproblem: <field>: <detail>, the way a draft prints its own, and it exits 2, the definition-problem code.--jsongives{status, file, site, actions, issues}.
Changed
- The mark is a cursor striking a site; the hero shows it. The tile is a
white cursor at the bottom left, a browser window at the top right and a
blue bolt inside the window where the cursor’s tip touches it. The favicon
is a heavier cut of the same tile that reads at 16 px. The landing’s hero
adds the cursor from the mark: it flies in, strikes the terminal’s prompt,
the transcript appears, it strikes the About window, and it holds; with
prefers-reduced-motionit rests at the prompt.assets/logo.svgis the one master;assets/src/mark.pystamps it into the wordmarks and the hero,assets/src/flow.pyreads it, andassets/social.htmlrenders the link preview from it. The README banners (assets/hero.png,assets/hero.ko.png) are rendered again and carry it in the wordmark. - The gate reads the request it is about to send, not only the declared
inputs. After the template is rendered, every query key and body field,
the value of an intent field such as
actionormode, and any GraphQL document inquery(orq/documenton a GraphQL path) are read again. A declared read whose rendered request asks for a change (ajson_templatewhose literal body is a mutation,{"op": "delete"}deep inside it) is treated as a change:approval_requiredcarriesreclassifiedwith the reason, the review shows it, and the audit record sayseffect: writewith the samereclassifiedreason. A read whose effect cannot be told (a GraphQL endpoint sent no document, a document itda cannot read) stops witheffect_unknown(exit 2) and a remedy; nothing is sent. A declared write is unchanged, and so are the 101 reads in the bundled definitions. itda describeshows the read a change names inconfirm, one line under the change in the person’s language (after a lost answer: checks list-notices for the new title);--jsonaddsconfirm: {read, match}. The change’s MCP tool description says the same in English, anditda_describein catalog mode now returns the same description as the tool, the approval note included.- Action descriptions are drafted in the person’s language, any of the
twelve, not only in Korean or English: a Japanese setup now writes
Hacker News の一覧を読む, notList what Hacker News shows. The language isITDA_LANGorLANG, else the page’s<html lang>, else English. The phrases are one table incrates/prep/src/label.rswith the same placeholders in every language (a test checks it); the site’s own words (titles, field and operation names) stay as the site wrote them, and outside Korean so does the kind of record (news articles). Agents read all twelve; tool names, error codes and remedies stay English.
Fixed
- A site whose only reads save files ended
itda setupas if nothing worked: setup skipped every file read (“saves a file”), so no read had answered and the definition was thrown away. Setup now tries a file read once, through the gate, by the answer’s headers and first bytes: the status, media type and size the definition allows count it as working, the rest of the answer is dropped and nothing is saved. A read answered with a page instead of the file is left out (wrong_media_type).itda checkstill skips file reads. - A same-origin redirect or a next-page address of the form
/graphql?query=mutation{...}passed the read check, because only the path and the query keys were read. The check now parses a GraphQL document inquery(andq/documenton a GraphQL path) for its operation type; a mutation or subscription is not followed or turned to. X-Original-URLandX-Rewrite-URL, which ask a server to treat the request as one to another address, are refused like the method override headers.- The SQLite database was created with the umask; only its directory was
private. The file and its
-wal/-shmcompanions are now 0600 on Unix (files left by an earlier run are tightened when the store opens); on Windows the directory’s ACL covers them.
0.5.0 - 2026-10-23
Uglier sites, fewer steps, and a bundle for Claude Desktop. itda setup now
reads pages whose content sits in frames, signs in with a password without a
browser, follows a session carried in the address, turns pages, recognises
known software, and refreshes a definition when the site changed. The landing
page says plainly what works and what does not. Every release now carries an
MCP Bundle per platform, so Claude Desktop installs itda by opening a file.
Added
-
Frames: when
itda draft pageoritda setupfetches a page that is a shell whose content lives in one or more<iframe src>(or an older frameset) on the same origin, and the page has nothing of its own to read, itda fetches each frame page as itself, on the same pacing and with the same session, and drafts from the frame pages as pages of the site, each under the frame’s own address. At most four frames are followed, at most two deep; a cross-origin frame, and a same-origin frame that answered with nothing, are listed in the notes and never fetched. The setup lines and the draft say what was followed (“Followed 1 frame: /frame/board.”).itda refreshfollows them again. -
Query sessions: a site that carries its session in the URL as a query key (
?sid=...) rather than a cookie now gets akind: browserauth entry withvalue_from: query:<key>.itda draft har,itda recordanditda setuprecognise it when a sign-in’s answer hands the key out (in the redirectLocationor on the first signed-in page) and every signed-in call carries it: every operation that sent it getsauth: session, the key is dropped from the operations’ inputs, and the value goes to the keyring, never into the definition.itda login <site>captures it from the sign-in the same way it captures a cookie. At run time the value is appended to the query of each request; it is redacted in results, error text, the audit log and the recording kept for a refresh (addresses,Locationheaders and the links in recorded pages).value_fromon abrowserentry now acceptsquery:<key>besidecookie:<name>, with a validation error for anything else. Because the value rides in URLs,docs/THREAT_MODEL.mdnotes it can reach server and proxy logs. The definition format is unchanged:spec_versionstays 1. -
cargo xtask check-changelog, run by CI: the changelog has one of each section per release, the newest release matches the workspace version, and the landing page and quickstart print the same version. CONTRIBUTING.md says what a minor and a patch version mean before 1.0. -
Sign in with a password, without a browser: a
loginblock on abrowsercredential names the site’s sign-in page and its user and password fields, anditda login <site> --user mereads the page as itda, fills the form in with the password you type (or pipe on stdin), sends it once with the hidden fields and cookies the page gave, and keeps the session the way a browser sign-in is kept. A redirect means the site accepted it; the form coming back is reported, never retried.itda setup <page> --user medoes the same when the page needs a sign-in (a redirect to a sign-in page, 401 or 403, or a “log in to see this” page that links to one), drafts from the page as you see it signed in, gives every actionauth: sessionand writes theloginblock from the form it found. Over MCP,itda_setupnever takes this path.--secretonitda loginnow names the API key path explicitly (api-keystays the name when the site has no sign-in form). -
examples/lab: Gitea, Redmine, WordPress, MediaWiki and a small legacy intranet (intranet/server.py, standard library only) in Docker Compose, pinned by digest, on fixed local ports, with one seeded account and a little content each, plus reviewed definitions inspecs/. WithITDA_LAB=1,apps/cli/tests/lab.rswalks each one fromitda setup --userthroughitda login, a read, a change approved in a terminal and the audit log. CI never runs it. -
examples/lab: three uglier intranets alongside the five, to measure what itda does on pages built the harder ways (intranet/framed.py,scripted.py,urltoken.py, standard library only).framed(8086) keeps the notices table two iframes deep, so setup from the home page finds only the shell; pointed at the frame it signs in and reads and writes the board, andspecs/framed.yamlruns to an approved change.scripted(8087) draws the table fromGET /api/noticesin a script, soitda setup --watch-onlywatches the browser load it and drafts the read, andspecs/scripted.yamlposts with theX-CSRF-Tokenfrom<meta name="csrf-token">.urltoken(8088) carries its session in?sid=with no cookie; with frame following and query sessions in this release, setup signs in, reads thesidfrom the sign-in redirect and keeps it as avalue_from: query:sidsession, andspecs/urltoken.yamlruns to an approved change.README.mdrecords the measured outcome per case; the one itda cannot do yet - signing in from a shell whose frame redirects to a login, soframedfrom the home page still finds only the shell - is a test marked#[ignore]with the reason. -
Sites that run known software are recognised.
itda setupreads the page’s HTML for GitLab, Jira, Confluence, Redmine, Discourse, MediaWiki, WordPress and Gitea or Forgejo (ageneratormeta, well-known asset paths, the Atlassian context-path metas, body classes), confirms the guess with exactly one GET to that software’s own version or info endpoint, and then drafts from a template undersites/software/instead of reading the page like any other: the reads an agent needs most, up to three changes marked as such, models and the software’s own way of signing in, every operation with its documentation inevidence. The template’sbase_urlis filled with the instance’s API base, context path included (/jira,/w, a GitLab relative URL root, a GiteaappSubUrl, the WordPress API root the page links to); the site is named after the host; one line says so (“This looks like GitLab. Using its API at.”). The page draft still runs and its lists, forms and feeds join the template, the same method and path drafted once, and a page that needs a sign-in or scripts still gets the template when the software is confirmed. A fingerprint the API does not confirm is dropped quietly. itda setup --as <software>forces a template,--no-templateskips them,itda checkworks on the result as on any site, anditda_setupover MCP takestemplateand names the template it used in its result. The site’ssources.jsonnames the template and its API base, soitda refreshfills the same template again, with the page read again when it still opens. -
itda refresh <site>: whenitda checksayssite_changed, draft the site again from the sources it was set up from and merge the result with the installed definition. The site’s name, title and address,defaults,authand every action edited by hand are kept; actions the draft changed take the new version, ones it no longer drafts are dropped only when their read also failed, and new ones are tried once and added when they answer. The diff is printed in plain words (added, removed, changed, kept as edited) and written after a yes, or at once with-y; the site is checked right after and the verdict printed. Approvals for the site are withdrawn, as for any replaced definition: what the user reviewed is no longer what would be sent. To make that possible,itda setup,itda draft ... --installanditda record --installnow writespecs/<site>/sources.jsonnext to the definition: the pages, the API description, the GraphQL endpoints asked, the recording when one was kept (copied under the site asrecording.harwith its cookie, token and session values, private fields and sign-in answers blanked), the draft’s options and a hash of every operation as drafted.itda checknow points atitda refresh <site>when a site changed, anditda check --allprints one verdict line per site. -
itda_manageover MCP gainsrefresh: the person confirms the diff in a form (a form nobody answers counts as no), and the site’s tools are reloaded and checked.itda_checkwithout a site already checks every site; the docs now say so. -
itda draft pageanditda setupread a page that is one thing (an article, a package, a product, a bug) as its own content: the headline, the body text, what sits beside the headline, a date, an author and the values the page labels in definition lists and label-value rows, ashtml.pagefields that each match once. Such a page used to be one read that checked the title. A page that is one thing with a named list of records on it (a package with its releases) gets both reads, anditda setupsays “the page’s own content” when it finds one. -
Repeated siblings of one shape become records when they share a title at one place: a link as before, or a heading, a text named as a title, or the record itself when it is a link with something inside. The fields come from what the records share: a summary, a byline, a date, and whatever sits under a class the site named (
score,author,release__versionbecomesversion), with build names left out and a text that reads as a date aspublished_at. Bare paragraphs, bare links and menus never qualify. A list read is described by what its records are (List the projects on Projects) when the page names them. -
A list whose links share one shape (
/item/{id},view.php?id=) gets a read of one record by itsid, with the query keys every link sends as inputs with defaults and no response check yet. -
The
pageinput follows the link markedrel="next"and starts at 0 foroffsetandstart. -
itda setupwatches what a page drawn by scripts loads by itself before asking anyone to record. When the page shows itda nothing but a title, carries scripts that could fetch something (a script file or a preloaded module, or an inline script that imports a module or mentionsfetch,XMLHttpRequest,graphqlorapi) and a browser is installed, it opens the browser on the page in the same fresh profile recording uses, waits until the network has been quiet for two seconds (ten at most), scrolls once to the bottom, waits again, closes the window and drafts from the recording the wayitda draft hardoes, the page’s own address first so what the page itself shows is never lost to a busier API elsewhere. One line says what it saw (Watched the page load: 12 calls, 3 reads drafted., in English or Korean, counting the reads beyond a title check); only when no such read came out does it ask to record as before. Nothing in the window is clicked or typed, nothing it signed in with is kept, and a page whose scripts go to a sign-in page stops with the sameauth_requiredwords a redirect gets.--no-recordskips the window too; a new--watch-onlywatches but never asks to record, for scripts. A site whose software is recognised gets its template instead and is not watched.itda_setupover MCP does the same, and says in its description and its result that a window opens briefly on the user’s screen. -
Reads that list things can turn pages. A definition declares
pagination(style: page|offset|cursor, where the next marker is in the answer as a JSON pointer orheader:link, an optionalmorepointer, and thequeryorbodyinput it goes back in). The answer then carriesmeta.next, an opaque handle such asic_…;itda call ... --next <handle>andnextover MCP (itda_calland the per-action tools, whose schemas list it) read the following page with the same inputs. The site’s own marker stays in the local database for fifteen minutes, bound to the site, action, account, inputs, definition and credential, and is removed from the data; acursorinput is never shown to agents. A handle for another call, or an expired one, is refused withpagination_invalidorpagination_expired(exit 23). Only reads turn pages.itda draft openapi,itda draft har,itda draft pageand the GraphQL schema drafts declare it when the evidence is plain, never by guessing; the bundledgithub(issues, comments, repository search),npm(search) andcrates-io(search) definitions declare it.AGENTS.md, the skill and the server instructions say to read only as many pages as the question needs. -
A preflight step can read a value from the JSON a page embeds for its scripts:
extract: {build_id: "ssr:__NEXT_DATA__#/buildId"}, so a token or build id that only lives there reaches the action’s path or inputs. Only a string, number or boolean counts. -
AGENTS.mdandskills/itda/SKILL.mdtell coding agents how to use itda: discover actions, run reads, treat website text as data, wait foritda approve, never retry a lost change, runitda checkfirst when something stops working. The MCP server’s instructions say the same, shorter. -
itda feedbackprints a link to a new GitHub issue with the version, the platform and the doctor checks filled in (never the data directory path) and opens it;--no-openonly prints. It sends nothing itself. -
itda checkends each site with a verdict and what to do:ok,signed_out,site_changed,blockedorunreachable. The exit code follows the worst verdict, and--jsoncarries it asverdictand per site undersites.
Changed
-
itda draft pageanditda setupon a page that refuses itda say what the gate saw instead oferror[internal]: <url> answered HTTP 403: the host is behind Cloudflare, runs a bot manager or asks for a captcha (blocked_by_site, exit 12), or limits how often it is read, with the wait it asked for when it named one (site_rate_limited, exit 13), in English or Korean. A captcha served with HTTP 200 is reported the same way, and a page that sends itda on to a sign-in page says so (auth_required, exit 10) and points at recording. -
A
Retry-After: 0, or a date already past, no longer counts as a rate limit: a site that sends it with every page is read as itself. -
The landing page on phones: lighter glass, the setup dialog left out of the hero stack, full-width buttons, tighter rhythm, terminal lines wrapped on anything narrower than the two-column layout, and the sky drawn smaller with the cursor effect only while a finger is down.
-
The landing page and both READMEs carry a prompt to hand to an agent: paste it into Claude Code, Cursor or Codex with a site’s address and the agent installs itda, runs
itda setup <address> --client <app> -yand starts using the site. -
The landing page has a living sky behind the whole page: liquid chrome with striped, colour-split reflections and a holographic sheen, a wire grid running to the horizon, and a field of points and threads that gather around the cursor, with a website on the left and an agent on the right (
site/sky.js, three.js from cdnjs). It follows the scroll, draws one still frame under reduced motion, and leaves the plain glow without WebGL. -
The hero lost its badge and chips; the binary, platform and licence line moved to the footer.
-
The landing page is laid out as a desktop. The hero stacks windows that can be dragged: the first run, the actions it found, the setup progress, an about box and a clock. Sections open with a numbered eyebrow and a centred headline, every box is a window with a title bar and a dither texture, and the risk comparison is two windows side by side, the browser-driving agent’s lane next to itda’s.
-
The sky is now a living network: nodes spring home through a slow current, threads join neighbours, packets of light run node to node and along the handoff between the website and agent hubs. The cursor is a node too: nearby nodes gather and orbit it, its motion leaves a wake, threads run from it, and a click sends a ring through the field. Node count follows the viewport, with a lower cap on touch screens.
-
The landing page’s components now speak the same language as the sky: smoked-glass panels with a chrome hairline and faint scanlines, chrome headlines with a holographic accent, glossy pill buttons, Aqua-style window lights on the terminal, and chrome coins for the step numbers.
-
itda <address>sets that site up, anditda github(or npm, crates-io, pypi) adds the ready-made site and connects the agent apps found on this computer; the README’s one line is the one line you type. The tagline and the landing page say so. -
action_outcome_unknownnow says that itda never resends a change on its own, and the runtime tests prove it against a server that drops the connection after taking the request. -
A bare
itdain a terminal says what it is and asks for one address, then runsitda setupon it, so the first run is: typeitda, paste, Enter. -
itda setupfinds the agent apps on this computer (Claude Desktop, Claude Code, Cursor, Codex, or a project with a.vscodefolder) and connects them all after one Enter; the numbered list only appears when none is found. -
itda setupasks whether the site needs a sign-in only when the page it fetched has a password form; a page without one is used as it is. -
The installer ends with “Open a new terminal window and type: itda”.
-
At the first prompt, a ready-made site’s name (
github,npm,crates-io,pypi) installs that site and connects the agent apps found on this computer. -
After adding itda to Claude Desktop on macOS, itda offers to restart the app when it is open, or says that opening it is enough; for Claude Code it runs
claude mcp additself whenclaudeis on the PATH. -
itda doctorlists the agent apps whose settings already start itda. -
itda setupon a page ofpypi.org,github.com,npmjs.com(with or withoutwww.) orcrates.iouses the ready-made definitionitda addinstalls for it, says “This is PyPI. Using its JSON API.” (or the site’s name and API), does not fetch the page and tries the definition’s reads;--no-templatereads the page instead.itda_setupover MCP does the same and puts the line in its notes.
Removed
- The
browser-fetchandbrowsertransports and thederivedcredential kind, left over from the port: no code ran them, and a definition that named them was accepted and then refused at run time.itda addnow refuses them with the field named.
Fixed
- Signing in from a shell whose frame redirects to a login. When a page is a
shell and its same-origin frame answers with a redirect to a sign-in page,
itda setupnow treats it as if the top-level page had: with--userit signs in through that form and reads the frame again with the session, then drafts from it, instead of drafting only the shell’s title. Following the frame’s redirect to read the login form is a read; nothing is posted without the person’s credentials and the usual consent. Without--user, setup names the sign-in the frame leads to, anditda draft pagenotes that the frame leads to a sign-in rather than only that it was not read. - Approving in the client works on MCP protocol 2026-07-28, which Claude
Code 2.1.289 negotiates: that revision took server-to-client requests
away, so the
elicitation/createrequest itda sent was dropped unseen and the call hung until itda gave up. itda now hands the form back as the call’s result (resultType: input_required), the client shows it, and calls again with the answer and the approval id asrequestState; the id only ever names an approval itda stored for this very call, and anything else is asked again. The confirmationsitda_manageanditda_loginput to the person go the same way. Clients on the earlier handshake keep the request they had. The review the form shows no longer starts with a blank line, and leaves “as account” out when the site has no accounts: Claude Code cut a five-line review to its first three and hid the request and the inputs behind “(+2 more lines)”, and shows the four-line one whole. - A change answered with a redirect (post, then redirect, then get, as
Gitea, Redmine and most server-rendered sites do) is reported as landed,
with the address the site pointed at, instead of
request_failed. A redirect to a sign-in page is reported asauth_required, since the site is asking for a session it no longer accepts. - The cookies a preflight step’s answer sets go along with the change, so a Rails site (Redmine) that rewrites its session cookie on every answer accepts the token it just handed out.
- A read redirected to an address itda will not follow, such as a sign-in
page with a
reauthparameter (WordPress), is answered as the redirect it is instead of failing withbad_url, soitda setupcan name the sign-in page. - Two itda processes starting on a fresh data directory at once (the MCP
server and
itda approve, say) no longer race in the database setup:Store::migratereads the schema version and applies the steps in one immediate transaction. - A GraphQL document nested deeper than 32 levels is refused before it is
parsed, with a clear message in a definition’s default and
bad_requestfor an agent’s input, instead of running the parser out of stack. api_denied(exit 14): a 403 that is not a sign-in problem says the site refused that one request as not allowed, withupstream_statusand, when the site’s JSON answer names one of a short list of reasons,provider_reason; nothing else from the answer is passed on. A 403 whose reason says the session was rejected staysauth_required. Every refusal now carriesupstream_status, anditda checkcounts an address answered with 404, 405 or 410 and a query answered with errors (query_failed, exit 11) assite_changed.approval_pending(exit 20): a call with an approval the user has not answered yet is told to wait and call again with the same id, with the command to run incommand; the approval stays. A call with other inputs withdraws the approval (approval_expired,reason: inputs_changed).itda rejectalso withdraws a granted approval, anditda logout,itda remove, a newitda loginand replacing a definition withdraw every approval for the site or account. The reviewitda approveprints, and the form an MCP client shows, name the action, say what it does in the definition’s words, and cut a value longer than 500 characters with its length.- A read is classified again after its preflight steps with the found values
filled in; one that would now be a change stops with
effect_changed(exit 2) and is not sent. Headers and inputs that override the method (X-HTTP-Method-Overrideand the like) are refused at install and before sending. A GraphQL answer that carrieserrorsbehind a 2xx isaction_outcome_unknownfor a change andquery_failedfor a read, never a success. itda auditanditda doctorshow achange_startedrecord with no outcome after it as a change with an unknown outcome (the process stopped after it was sent), anditda_manage auditreturns them underunknown_outcome. A change that failed before anything left the machine (no turn at the gate, a name that did not resolve, a refused connection) keeps its network code and is reported as not sent instead of unknown.- Long secrets are sealed into
vault.sealedunder the data directory (AES-256-GCM, a fresh nonce per write) with one 32-byte master key per data directory kept in the system keyring; every browser session goes there, and so does anything over 1024 bytes or that the keyring refuses as too long (Windows Credential Manager caps a credential atCRED_MAX_CREDENTIAL_BLOB_SIZE, 2560 bytes). API keys stay in the keyring. Existing entries keep working and move on their next write, in both directions;itda doctorsays where secrets are. The cipher comes fromaws-lc-rs, already built throughrustls. itda describe <site>[.<action>]prints each action’s inputs and a command to copy, with a placeholder for every input it cannot run without;itda ls --jsoncarries the same command asexample. An unknown site, action or input names the nearest one (“Did you mean”), in the CLI and over MCP.itda mcp --site <name>serves one site only: its actions are the only tools,itda_setupis refused, and the other management tools stay inside it.- Two more ways to install, from the next release on. Every release carries
one MCP Bundle per platform (
itda-<target>.mcpb): open it and Claude Desktop installs itda with its tools, no terminal needed; approving a change still happens in a terminal withitda approve. The bundle is packed bycargo xtask mcpbfrom the release archives, with a manifest that follows the MCPB spec (manifest_version0.3,server.typebinary,itda mcpas the command). A winget manifest for 0.4.0 (manifests/b/brnyxx/itda/) is ready to submit to winget-pkgs and is not submitted yet. - Five MCP tools so an agent never needs a terminal to set a site up or
look after it:
itda_setup(look at a page as itda, draft, try every read once, keep what works; the site’s actions become tools in the same session, announced withnotifications/tools/list_changed, which the server now declares withtools.listChanged),itda_login(the person signs in in their own browser, after a form in clients that show one; the definition must name the sign-in cookie),itda_check(the verdict per site, asitda check --json),itda_sites(what is set up, with the last check’s time and verdict, now kept inspecs/<site>/check.json) anditda_manage(removeandresumeafter the person confirms in a form,approvals,audit).itda_setupnever asks anyone to record and never keeps a credential; pages that need a sign-in, or still show nothing after the watched load above, are refused with the command to run instead. The server instructions,AGENTS.mdand the skill say when to call them. - A new
itda-setupcrate holds the steps behinditda setup,itda login --browser,itda checkand the installed sites, with the questions answered through a trait and progress reported through a callback, so the command line and the MCP server run the same code. The CLI’s words and questions are unchanged. itda draft pageanditda setupdraft a typed read for each list they find in a page’s embedded data, instead of one read that returns the whole payload: anItemListor a list of one typed thing (NewsArticle,Product…) inld+json, and in__NEXT_DATA__,__NEXT_F__,__APOLLO_STATE__,__PRELOADED_STATE__and the like the list of objects with the most to read. Each read is named after what it holds (list-news-articles), pointsextract_pointerat the list and keeps up to eight plain fields of every record through amodel, ids and titles first and credentials left out.itda checkcounts its records. The whole payload is drafted only when no list is in it.- An Apollo cache (
__APOLLO_STATE__) is also grouped by typename, so a definition can point at/Questionor/Product. defaults.intervalin a site definition asks for more time between requests to that site (2s,500ms, up to 30 s). itda’s own pace, one request at a time per site about 1.2 seconds apart, is the floor; a definition can only slow it down. A value that is not a time is refused byitda add.- A site that answers a read with 429 or 503 and a
Retry-Afterof ten seconds or less gets that wait, once, and the read is tried again. A longer wait stops the read withsite_rate_limited,itda checkreports the site asblocked, and the error, the check result (retry_after_s) and the MCP server instructions say how long the site asked for. A change is never sent twice. - Requests queued in one process now take turns in order instead of polling the shared gate against each other.
- A draft that sees a GraphQL endpoint (a recorded call to it, or a page
naming a same-origin
/graphqlpath) asks it once, with the standard introspection query, what it offers, and drafts one read per rootQueryfield whose arguments are scalars, enums or ids: the field name is the action, the arguments are typed inputs, and the selection is the field’s scalars two levels deep, 24 at most. An optional argument that holds an object or a list is left out and listed; a field that requires one is listed instead.Mutationfields are listed in the notes and never drafted. A site with introspection off is drafted from its pages and calls alone. The query is sent as itda, read-only, with a size cap, byitda draft page,itda draft har,itda recordanditda setup. - An optional input the agent leaves out now leaves the
json_templatebody too, instead of standing in as an empty string where a number or null belongs; GraphQL variables typedIntorBooleanneed that. itda draft pageanditda setupread the API description a site publishes: the one the page links to, else/openapi.jsonon the same origin when it answers with one (one GET, as itda, at most 4 MB). Its reads join the draft the wayitda draft openapimakes them, drafted once when the page already had the same path; its changes become notes, and a read keyed by a credential input is left out and listed. A description on another address is noted, not merged, and one that does not parse leaves the page draft as it is. The header comment and each merged action’sevidencename where it came from, anditda setupsays what it read in one line.- Requests send
Accept-Language: *(no preference) instead ofko-KR,ko;q=0.9,en;q=0.8, a value left over from the browser header block itda was derived from. pypi.org answered a project page asked for with any KoreanAccept-Language(ko,ko-KRor that list) with a 3 KB “Client Challenge” page and kept challenging every request from the same address for a few minutes after, soitda draft page https://pypi.org/project/requests/drafted the challenge instead of the 322 KB page. Leaving the header out is not enough: amazon.com then sends a gzip body itda does not decode. A definition that needs a language can still set one indefaults.headers. - A challenge page served with HTTP 200 (small, titled as a challenge, only
a script and no link to follow, as bugzilla.mozilla.org’s bug list and
pypi.org answer when they challenge) now counts as a block: the site is
paused,
itda draft pageanditda setupsay the site’s bot protection refused itda (blocked_by_site, exit 12), anditda checksaysblocked. Before, it was drafted as a read named after the challenge and checked asok. - Drafts leave ad slots out of a page’s records (an element whose class, id
or label marks it as an ad:
AdHolder,ad-*,sponsored,aria-label="Sponsored"): when ads share their classes with the records beside them, the records’ selector excludes the ad class. On amazon.com the draft had picked the sponsored results, which are not there when the first page is asked for by number. - The page input a draft adds when a page links to its next page (
page,offset,startand the like) has no default any more, so the read anditda checkask for the address the draft saw until an agent or a page handle asks for another page; the next page is still counted from 1, or from 0 for an offset. Sendingpage=1made some sites answer with different markup and the check fail. - A read the site blocks or challenges while
itda setuptries it is left out with “: the site does not give itda this page”, nothing more is sent to the site in that run (the reads after it are kept and shown as not tried, for the next itda check), and the site is not left paused, so a challenge on pypi.org’s search page no longer takes the three feeds after it down too.itda checkand calls on an installed site still pause it.
0.4.0 - 2026-10-22
One address is enough: itda setup looks at a page, or watches you use the
site in your own browser, names what agents will be able to do in plain words,
tries every read, and connects an agent. Any website, not only the ones with an
API description.
Added
itda setup <url>sets a site up in one go: it looks at the page, or opens your browser and records when the page refuses, needs a sign-in or is drawn by scripts; shows what agents will be able to do in plain words; tries every read once and keeps the ones that work; saves the site; and connects an agent. It asks before each step;--record/--no-record,--clientand--yesanswer for scripts.ITDA_LANG=ko(or a KoreanLANG) makes it, and the drafted descriptions, speak Korean.- Drafted actions carry a description a person would say (
List what Notices shows,공지사항 목록 읽기), taken from the page’s title or the call’s name, in English or Korean. itda record <url>opens your own browser in a fresh window and records what the pages ask the server for while you use the site, then drafts a definition the same wayitda draft hardoes. The recording stays in memory;--savekeeps it as a HAR file. itda never clicks, types or navigates in the window.ITDA_DEBUG_CDP=1prints the browser messages.itda record --installoffers to keep the token and the browser session the recording signed in with, each after a yes in the terminal, so a site that sends a bearer token from its scripts works without pasting it.- A recorded GraphQL call becomes one action per operation (
exploreProjectsisexplore-projects): queries are reads, mutations are changes, and the variables are the typed inputs, rendered through ajson_template. itda recordanditda draft haralso draft the pages the recording holds, asitda draft pagedoes, so a site that renders its lists and forms on the server gets actions even when its pages call no API.itda draft page <url>...drafts a site definition from the pages you use, fetched with the honest client: a table or a list of records becomeshtmlfields, data a page embeds for its scripts becomesssr_json, a search form becomes a read, a post form becomes a change with its token read through a preflight step, page links become apageinput, and RSS or Atom feeds come along. Every read gets a response check, soitda checknotices when a page changes. Lists are found by their shape, not only by known class names: any run of three or more siblings outside navigation that each hold a link at the same place, with the title and link read from that link’s own path. When a page embeds its data for its scripts, the draft pointsextract_pointerat the list inside the payload and checks the keys every record shares.itda draft haralso handles a token sent as a header, such asX-CSRF-Token: a preflight step reads it from the page’s<meta name="csrf-token">tag before each call.itda addalso takes an https URL, or the name of a ready-made site (itda add github); the four definitions insites/are built in.
Changed
itda setupnames the agent app it connects (“Adding itda to Claude Desktop…”), shows the settings file it wrote with~for your home folder, says so in Korean too, lines the action names up only as wide as they need, and writes its example request mid-sentence.- The README and the landing page open with a recording of
itda setupas it ran on Hacker News (assets/setup.gif, Korean inassets/setup.ko.gif, rebuilt fromassets/src/setup.*.tapewith vhs). - A new key visual, a hand passing a website card to an agent
(
assets/handoff.png), on the README banners and the link preview (assets/hero.png,assets/hero.ko.png,assets/social.png, rendered fromassets/src/banner.html), and the docs introduction says the same thing the README does.
0.3.0 - 2026-10-20
Install ready-made sites with one command, and work with old intranet forms that need a fresh security token on every change.
Added
- Preflight steps: an action can read a page just before it runs and pass
a fresh value, such as a CSRF token, into its headers, query or form as
{{step.<step>.<name>}}. Agents never see or set those values. - The sandbox has a write form with a one-time CSRF token
(
sandbox.write-notice) to try preflight steps on. itda draft harturns a form token such as_csrfinto a preflight step that reads it from the form page recorded before the post.itda add <file>installs a site definition (or-for stdin) and shows where it sends requests, which actions change the site and how it signs in;itda remove <site>takes it away.- Ready-made site definitions in
sites/: GitHub (public reads without an account, issues and comments with a token), npm, crates.io and PyPI.
Changed
- A
modelnow applies to each record when the answer is a JSON list, instead of returning an empty object.
0.2.0 - 2026-10-20
Sign in through your own browser, approve changes inside the agent app, and check that sites still answer the way their definitions say.
Added
itda accountslists signed-in accounts and the names of their secrets;itda logout <site>removes an account and its saved secrets.itda check [<site>]runs every read that needs no input and reports which ones no longer match the site, with exit code 11 when a site changed.- Browser sign-in for sites without API keys:
itda login <site> --browseropens your own browser in a fresh profile, keeps only that site’s session cookies in the system keyring, andkind: browseractions send them only to the same site. - Approve changes inside the MCP client: with
itda mcp --approve-in-client(oritda mcp install <client> --approve-in-client), clients that show elicitation forms ask you to approve the exact request, and declining returnsapproval_rejected.
0.1.0 - 2026-10-19
The first release: reads and approved changes on plain HTTP sites with API keys, from the command line or over MCP, and definitions drafted from OpenAPI documents or browser recordings.
Added
itdacommand:ls,call,login,resume,doctor, with a stable exit code contract for scripts and agents (2input,10sign in,11page changed,12blocked,13rate limited,20–22approvals and changes,30network).- MCP server (
itda mcp): one tool per action up to 30 actions, three stable catalog tools (itda_search,itda_describe,itda_call) above that. Results are marked untrusted and cut to 24 KB. itda draft openapi: draft a site definition from an OpenAPI 3 or Swagger 2.0 document, with response checks, header auth and a list of what could not be translated.--installputs it in the data directory.itda draft har: draft a site definition from a browser recording of you using the site. Only the shape of each call is kept; recorded tokens, cookies and values never reach the draft.itda mcp installfor Claude Desktop, Cursor, VS Code and Codex, and theclaude mcp addcommand for Claude Code. Existing settings are kept and backed up.- Approvals for changes: a change returns an approval id, the user reviews
the exact request with
itda approvein a terminal, and the change runs once with exactly those inputs.itda approvalsanditda rejectlist and refuse them. - Hash-chained audit log of every call, approval and change, with
itda auditanditda audit --verify. Input values and credentials are never written to it. - Credential vault on the OS keyring, or
ITDA_SECRET__environment variables for CI and containers. Agents only ever see account ids. - Site definitions (
spec.yaml): JSON and server-rendered HTML responses, declared response checks, models, and safety validation with named problems. - Network layer: honest
itda/<version>user agent, per-site pacing and budgets, pauses on blocks and expired sign-ins, private-address guard, credentials bound to their origin. - Sandbox example site for trying everything locally.