# TVclRestAgent

An HTTP/JSON interface for remote-controlling and inspecting a running VCL
application.

*Formerly CAWRestRemoteControl. Same component, standalone name — it no
longer carries the prefix of the product line it was written in.*

**What it is for:** driving and checking an application automatically,
without depending on screenshots and window coordinates. Instead of "click
pixel 530/342", the caller asks which actions exist and triggers the right
one by name. Instead of guessing whether an input arrived, it reads the
property back.

Behind the interface there is nothing but HTTP and JSON, so whatever is on
the other end is none of its business: a shell script, a scheduled task, a
test runner in any language, a colleague with `curl`. Typical uses beyond a
test suite:

- a **scripted regression run** against a sealed-off installation, driven by
  names instead of coordinates and therefore still working after the next
  layout change
- a **smoke test after deployment** — does the application come up, does it
  reach the database, is the screen that matters actually there
- a **legacy application with no interface of its own**, made scriptable
  from the outside without touching its logic
- **diagnosis of a running installation**: which form is in front, what is
  in that field, what went wrong — without attaching a debugger

The [permission areas](#permissions-per-area) earn their keep in exactly
those cases: a smoke test needs information and nothing else, so everything
else stays closed.

Delphi on-board means only (Indy). No DataSnap, no WebBroker, no URL
reservation, no administrator rights.

---

## Setting it up

Drop the component on a data module (palette **VCL REST Agent**) and
switch it on:

```pascal
RestAgent.Port := 8377;
RestAgent.BindAddress := '127.0.0.1';
RestAgent.Active := True;
```

Reading these values from wherever the application keeps its settings is
usually the better idea, because it keeps the interface **off** unless
someone deliberately enables it. The component has no opinion on where the
settings come from — it only has properties.

### Path prefix

`BasePath` puts a common segment in front of every path. Without it
(the default) you get `GET /focus`; with `BasePath := 'api'` it becomes
`GET /api/focus`. This applies to the built-in endpoints and to the
application-specific ones alike (`/api/custom/sql`).

Spelling does not matter: `api`, `/api` and `/api/` mean the same thing, and
the comparison ignores case. Requests outside the prefix are answered with
404.

Useful when the application already serves other HTTP paths, or when a
front-end server dispatches by prefix.

### Authentication

`AuthMode` decides how a caller has to identify itself:

| Mode | Requires | Sent as |
| --- | --- | --- |
| `amNone` | nothing | — |
| `amToken` | `AuthToken` | `Authorization: Bearer …` or parameter `token` |
| `amBasic` | `AuthUser`, `AuthPassword` | HTTP basic |

```pascal
RestAgent.AuthMode := amBasic;
RestAgent.AuthUser := 'tester';
RestAgent.AuthPassword := 'secret';
```

If `amToken` has no secret configured, or `amBasic` has no user name, the
request is **rejected** rather than let through. A forgotten setting should
produce a closed door, not an open one that looks like working protection
from the outside.

Setting only `AuthToken` and leaving `AuthMode` alone yields `amToken`
automatically, so older code keeps working. An explicit choice is never
overridden — which is why `AuthMode` has to be assigned **after**
`AuthToken` when both come from stored settings.

The secret travels as `Authorization: Bearer <secret>`, the way every HTTP
client already knows. Custom `X-` headers have been deprecated since
RFC 6648, so there are none here. The `?token=…` parameter remains alongside
it: it is the simplest thing that works in a browser or a one-liner, and it
works on every request, including a POST that carries a form-encoded body.

```
curl -H "Authorization: Bearer secret" http://127.0.0.1:8377/ping
curl http://127.0.0.1:8377/ping?token=secret
```

With `amBasic`, a rejected request also carries
`WWW-Authenticate: Basic realm="VclRestAgent"` — without that header
a browser will not prompt for credentials.

Indy does not offer more than this without the effort outgrowing the point:
digest would need nonce bookkeeping, anything token-based beyond that needs
a counterpart handing out keys.

> With `amBasic`, user name and password travel **unencrypted**, merely
> Base64-wrapped. On `127.0.0.1` that is of no consequence; across a network
> it would be.

### Security

The interface can drive the application and read its data.

- The default binding is `127.0.0.1` — **not** reachable from outside.
- Binding to `0.0.0.0` belongs in a sealed-off test environment, nowhere else.
- No `Access-Control-Allow-Origin` header is sent unless you ask for one
  (see below). Loopback is no protection against a browser: the browser runs
  on the same machine.
- Every endpoint answers only the methods it declares. `GET /terminate`
  is a 405, not a shutdown.
- `Execute` performs **no** authentication check — that call comes from
  inside the application itself.

### Browser access: `AllowOrigin`

Empty by default, and deliberately so. With a permissive
`Access-Control-Allow-Origin` header, any web page the developer happens to
visit could drive this application through the browser **and read the
answers**.

Set it only for a tool that genuinely runs in a browser, and then to that
tool's origin rather than to `*`:

```pascal
RestAgent.AllowOrigin := 'http://localhost:5173';
```

With it set, the component also answers the browser's preflight: an
`OPTIONS` request is let through without credentials and replies `204` with
`Allow`, `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers`.
Without it, `OPTIONS` sits behind authentication like everything else.

### Permissions per area

The endpoints are grouped into areas, and each area has one level. This lets
you tailor an installation without securing every single call.

| Level | Effect |
| --- | --- |
| `acDenied` | The endpoint answers **403**. |
| `acReadOnly` | Information only; anything that changes something gives **405**. |
| `acFull` | No restriction. |

| Property | Endpoints | Default |
| --- | --- | --- |
| `InspectAccess` | `/components` `/component` `/objects` `/actions` `/screenshot` `/wait` `/machine` | `acFull` |
| `ControlAccess` | `/property` `/focus` `/action` `/click` `/key` `/mouse` `/dialogs` `/window` | `acFull` |
| `DatasetAccess` | `/dataset` | `acFull` |
| `FileAccess` | `/files` `/file` `/directory` `/rename` | **`acDenied`** |
| `DiagnosticAccess` | `/exceptions` | `acFull` |
| `LifecycleAccess` | `/terminate` `/restart` | **`acDenied`** |
| `CustomAccess` | `/custom/…` | `acFull` |

The area follows the **subject**, the level follows the **effect**. That is
why `/property` sits under `ControlAccess` and not under `InspectAccess`:
writing changes the interface, while reading stays possible at
`acReadOnly`.

`/ping`, `/nop`, `/time` and `/openapi.json` have **no** setting — otherwise
you could not tell a sealed-off installation from a dead one. They still sit
behind authentication. `/machine` deliberately does not belong to that
group: network adapters and addresses are reconnaissance material.

`/custom/` can be closed with `CustomAccess`, but `ReadOnly` does **not**
reach inside it, because the component cannot judge what happens behind the
event. The handler asks `TVclRestAgent(Sender).ReadOnly` itself.

### ReadOnly as a cap

`ReadOnly := True` pulls **every** area down to information only, without
overwriting the setting — switch it off and what was configured applies
again. A denied area stays denied; the cap never opens anything.

```
effective = Min(area level, ReadOnly ? acReadOnly : acFull)
```

### Methods are enforced

Each endpoint declares the methods it answers to. Anything else gets **405**
with an `Allow` header naming what is left — and that list already has
`ReadOnly` folded in, so at `acReadOnly` a dual-purpose path reports `GET`
alone.

This is not pedantry about HTTP. Without it, a single
`<img src="http://127.0.0.1:8377/terminate">` on any web page would shut the
application down, and a browser preflight would trigger an action.

`HEAD` is answered like `GET`. `OPTIONS` reports the methods in force
without touching the application.

---

## Built-in endpoints

All responses are JSON, shaped `{"ok":true,"data":…}` or
`{"ok":false,"error":"…"}`. A failure that still carries a result — a
timed-out `/wait`, say — has `ok:false` **and** `data`.

This chapter covers inspecting and operating the user interface. Everything
beyond that has its own chapter further down:
[Working with datasets](#working-with-datasets),
[Files and directories](#files-and-directories),
[Recording exceptions](#recording-exceptions),
[Environment and time](#environment-and-time),
[Shutdown and restart](#shutdown-and-restart) and
[Self-description: OpenAPI](#self-description-openapi).

Which of them are reachable at all in a given installation is decided by
[Permissions per area](#permissions-per-area).

### Inspecting

| Path | Purpose |
| --- | --- |
| `GET /ping` | Sign of life: application name, main form, number of forms |
| `GET /focus` | Active form, focused control, its parent chain |
| `GET /components?form=X&depth=N&props=…` | Control tree with class, name, position, size, visible/enabled |
| `GET /objects` | Every component the application holds, visual or not |
| `GET /component?name=X` | All published properties of a component (RTTI) |
| `GET /property?name=X&prop=Y` | A single property including its type |
| `GET /actions` | Every action with caption, enabled, visible, owner |
| `GET /dataset?name=X&max=N` | Fields and contents of a dataset |
| `GET /screenshot?target=…` | Screenshot (see below) |
| `GET /mouse` | Mouse position and the control underneath |
| `GET /window` | Window state, bounds, and whether the application is in front |
| `GET /wait?…` | Waits until a condition holds (see below) |
| `GET /dialogs` | Log of intercepted message dialogs |

#### The catalogue: `/objects`

`/components` answers a question about the user interface — what sits
where. It walks the control tree, so it sees only what has a place on
screen: a dataset, a timer, an action list are invisible to it, and data
modules do not appear in it at all.

Those components can be addressed by name through `/component`,
`/property` and `/dataset` — but without `/objects` there is no way to
find out what they are called, and "ask, don't guess" would end at
"guess what the dataset is called".

```
GET /objects
GET /objects?class=TFDQuery
GET /objects?class=TDataSet&derived=1
GET /objects?owner=dmMain
```

```json
{"name":"cdsArticle","class":"TClientDataSet","owner":"dmMain",
 "path":"dmMain.cdsArticle","visual":false}
```

`path` is what you feed back into the other endpoints. It is not
decoration: two data modules with a `cdsData` each are a normal thing,
and a plain name would reach whichever of them comes first — an order
that follows form activation and is not stable.

The answer reports `count` and `truncated`; at most 5000 entries come
back, and the class filter is the better tool long before that.

#### Filtering by class

`class` and `derived` work the same way on `/objects`, `/components` and
`/actions`:

| | Effect |
| --- | --- |
| `class=TEdit` | exactly that class |
| `class=TCustomEdit&derived=1` | that class and everything descending from it |

`derived=1` is what makes "list every dataset" possible without knowing
whether the application uses `TFDQuery`, `TADOQuery` or something of its
own. Classes are matched **by name**, because the component cannot know
the classes of the application it sits in.

On `/components` the filter prunes the tree rather than flattening it:
branches that lead to no match disappear, the way to a match stays. "Where
do the grids sit" is a question about the tree, not about a list.

#### Addressing an element

`name` accepts the plain name (`btnSave`), the full path
(`frmMain.pnlLeft.btnSave`) or the tail of one (`pnlLeft.btnSave`). A dotted
name is matched **against the path only**: whoever spells out a path means
that one, and gets a 404 if it does not exist — rather than the first
control of that name on some other form.

The same rule holds for components that are not controls: `dmMain.cdsData`
addresses that dataset and no other. `/objects` hands out exactly these
paths, so a catalogue run needs no guesswork about which one it got.

`form` matches the form's name or its class. A form that was named but not
found gives 404, not an empty list.

#### Properties inside the tree

`props` adds published properties to every node:

| Value | Effect |
| --- | --- |
| *omitted* | the tree only (default) |
| `props=1` | all published properties |
| `props=Caption,Color` | only those listed, case-insensitive |

Measured against an article screen with 275 controls: roughly 34 KB without
`props`, about 521 KB with `props=1`, around 45 KB with
`props=Caption,Color`.

Why that is not the default: the tree usually serves orientation, and
reading a property is **not guaranteed to be free of consequences** — on a
`TWinControl`, `Caption` goes through `WM_GETTEXT` and may create the window
handle on the way; other getters calculate or touch datasets. Whoever asks
for the properties accepts that knowingly; plain information about the tree
stays clear of it.

#### Lists inside a property

A property that holds a `TStrings` — `Items` of a combo box or a list box,
`Lines` of a memo — comes back as a **JSON array of its lines**, both in
`/property` and in the tree:

```
GET /property?name=cboCountry&prop=Items
{"ok":true,"data":{"name":"cboCountry","property":"Items",
 "type":"TStrings","value":["Germany","Austria","Switzerland"],
 "count":3,"truncated":false}}
```

That is the one object property worth unpacking: without it a caller can set
`ItemIndex` but cannot find out **which index it needs**, and "is the value
even on offer" stays unanswerable. Every other object property — `Font`,
`Constraints`, a nested component — is named and left alone, or the answer
would grow without end.

At most 1000 lines are delivered. `count` names the real number and
`truncated` says whether the cap bit, so half a log memo never looks like a
whole one.

### Operating

| Path | Purpose |
| --- | --- |
| `POST /action?name=X` | Execute an action |
| `POST /action?name=X&async=1` | Trigger it without waiting for it |
| `POST /click?name=X` | Trigger a control's `Click` |
| `POST /key?control=X&text=…` | Type into an element, wherever the window is |
| `POST /key?key=ENTER&ctrl=1` | A single key with modifiers, as real input |
| `POST /mouse?x=…&y=…&click=left` | Move the mouse and click |
| `POST /mouse?control=X&click=left` | Move to a control's centre and click |
| `POST /property?name=X&prop=Y&value=Z` | Set a property |
| `POST /focus?control=X` | Set focus, scrolling the control into view |
| `POST /window?state=maximized` | Change the window state, bring it forward |
| `POST /dialogs?answer=yes` | Answer the next message dialogs |

`/action` and `/click` answer **409** when the target is disabled or
invisible. That is deliberate: triggering a disabled button would do nothing
in the application while leaving the caller under the impression that
something happened.

`/click` goes through the `Click` method rather than simulated mouse events.
That always hits the intended element, regardless of whether the window is
in front or covered.

Every yes/no parameter — `async`, `raw`, `append`, `recursive`, `force`,
`idle`, `ctrl`, `shift`, `alt`, `foreground` — accepts `1`, `true`, `yes`
and `on`. Anything else counts as not set.

### Typing: two ways in, and the difference matters

```
POST /key?control=edtTotal&text=12,50     to the element
POST /key?key=ENTER&ctrl=1                as real input
```

> A comma needs no encoding — only `&` separates one parameter from the
> next, and a decimal comma is exactly what a German user interface
> expects. What does need encoding in a value: `&` as `%26`, `#` as `%23`,
> `%` as `%25`, and a literal plus sign as `%2B`, because a bare `+` is
> read as a space the way form data has always encoded it.

**With `control=`** the keystrokes go straight to that element's window.
That works while the application is in the background or covered, always
reaches the intended element, and the answer only goes out once the control
has processed the input — so reading the value back afterwards is
deterministic. This is the dependable choice, and for the same reason
`/click` goes through `Click` rather than through simulated mouse events.

The price: it bypasses the input queue and with it everything that hangs off
real keyboard handling — `KeyPreview`, `Application.OnMessage`, a modifier
held down. Modifiers are therefore **refused** there rather than silently
dropped, the sibling of "setting is not typing".

**Without `control=`** the keystrokes are real system input and go wherever
Windows thinks the focus is. That is the only way to send `Ctrl+S` or a
system key — but it needs the application to be in front, and if it is not,
this call answers **409** rather than typing into a stranger's window. Add
`foreground=1` to have the application brought forward first; if Windows
refuses that (it may — see below), the answer is still 409.

### The window itself

Whether the application is in front is not a published property; it is a
question to the operating system, so `/property` cannot answer it.

```
GET  /window                              the main form
GET  /window?name=frmArticle              a particular form, by name or class
POST /window?state=maximized              normal, minimized, maximized
POST /window?foreground=1                 bring it to the front
```

`GET` reports state, caption, visibility, bounds, window handle and
`foreground`. `POST` applies the state, then the front, and answers with the
values **read back from the window** — the same rule `/property` follows.

> Windows only lets the process that currently owns the foreground window
> hand it on. A background process asking for it gets a flashing taskbar
> button and nothing else. The component uses the customary workaround
> (attaching to the foreground thread's input queue) and then **asks again
> whether it worked** — if it did not, the answer is 409 with the state
> alongside it. A 200 here means the window really is in front.

The window state alone is also reachable through `/property`
(`prop=WindowState&value=wsMaximized`), because it *is* a published
property. `/window` exists for the part that is not.

### Waiting instead of polling

Without `/wait` a caller has to sleep after every action and look again —
precisely the timing dependency that makes automated runs fail sporadically.

```
GET /wait?form=frmArticle&timeout=5000
GET /wait?form=frmDialog&state=gone           until it has closed
GET /wait?control=btnPost&state=enabled
GET /wait?idle=1                              until the main thread is free
```

`state` accepts `exists` (the default), `gone`, `visible`, `hidden`,
`enabled`, `disabled` and `focused`, for a form as well as for a control.
An unknown state is answered with **400** rather than treated as "it
exists" — for a tool whose whole purpose is to take timing guesswork out of
a test run, a false "condition met" is the most expensive answer there is.

The answer is **200** with `met: true` when the condition held, otherwise
**408** with `ok: false` and `met: false` — a condition that never came true
is a failure for the caller, not a result. `waitedMs` reports how long it
actually took and comes with either answer.

### Triggering actions that open a modal dialog

An action that opens a modal dialog would never return: the call waits for
the main thread, the main thread waits for the dialog. `async=1` queues the
action and answers **202** immediately.

Checked first, though: a disabled action is still refused with 409 rather
than queued. The answer says the action was *started*, not that it
succeeded — what came of it is what `/wait` and `/dialogs` are for.

### Answering message dialogs

```
POST /dialogs?answer=yes&count=1
GET  /dialogs
DELETE /dialogs
```

| Parameter | Meaning |
| --- | --- |
| `answer` | `yes`, `no`, `ok`, `cancel`, `abort`, `retry`, `ignore`, `all`, `close`, or `none` to stop |
| `count` | How many dialogs the answer applies to, at least 1; unlimited when omitted |
| `form` | Only dialogs of this class or name, for this arming |

The answer is delivered through the **button**, not by setting
`ModalResult` — only that way does the logic hanging off `OnClick` run as
well. Every intercepted dialog is recorded with its class, caption and text;
that text is often worth more than the click, because it says what the
application asked.

`GET /dialogs` reports the log, whether the watcher is armed, and the
filter **in force**. `DELETE` clears log and arming and reports in `cleared`
how many entries it discarded.

> **`DialogFormFilter` decides whether any of this works.** The default is
> `TMessageForm`, the class behind `MessageDlg` and `ShowMessage` — and for
> most grown applications the wrong one. If your application has its own
> message dialogs, name their classes once, as a semicolon-separated list:
>
> ```pascal
> RestRemoteControl.DialogFormFilter := 'TMyMessageDialog;TMessageForm';
> ```
>
> Get this wrong and the watcher runs, reports `watching: true`, and steps
> over every dialog without a single error anywhere. `GET /dialogs` reports
> the filter in force for exactly that reason. `*` catches every modal
> dialog, which includes genuine working dialogs — so it has to be asked
> for explicitly. Clearing the property is honoured as "match nothing"
> rather than quietly falling back to the default.

### Setting properties

```
POST /property?name=edtTotal&prop=Text&value=12,50
```

The answer contains the value **read back from the object**, not a
confirmation. Some properties clamp or reformat what you give them, and that
is precisely what the caller needs to know.

Integer subranges with named values are resolved: `Cursor=crHandPoint` and
`Color=clWindow` work alongside their numeric equivalents.

> Setting is not typing. A property set through the interface does not raise
> the same events as a real keystroke. Where validation hangs off
> `OnChange`, use `/key` instead.

### Screenshots

| Parameter | Meaning |
| --- | --- |
| `target=mainform` | Main form (default) |
| `target=activeform` | Active form |
| `target=screen` | The whole screen |
| `target=handle:12345` | A specific window |
| `target=<name>` | A form or control by name |
| `file=…` | Write to a file below `FileRoot` instead of returning it |
| `raw=1` | Return `image/png` directly instead of Base64 in JSON |

Windows are captured with `PrintWindow`: the content is read from the window
itself, so the image is right even when the window is covered or in the
background. A target that does not exist is answered with **404** — not with
a picture of the whole desktop.

> `file=` is **writing a file**, and it goes through the same gate as every
> other file: the path is relative to `FileRoot` and `FileAccess` has to be
> `acFull`. Without that, this one parameter would be the way around the
> entire file permission.

---

## Working with datasets

`GET /dataset?name=X` returns fields and rows; `at=current` returns only the
record the application is sitting on. `POST` navigates and edits:

| `op=` | Effect |
| --- | --- |
| `first` `prior` `next` `last` | Navigation |
| `locate` | Search; field values in the JSON body, result in `found` |
| `insert` `edit` | Create or change; field values in the body, `Post` included |
| `post` `cancel` | For when the user interface already left the dataset in an editing state |
| `delete` | Delete |
| `refresh` | Re-read |

```
POST /dataset?name=tblData&op=insert
Content-Type: application/json

{"article_no":"4711","name":"Example article"}
```

### Values keep their type

What comes out is what goes back in, without the caller converting
anything:

| Field | In the JSON |
| --- | --- |
| Integer kinds | a JSON number |
| Float, currency, BCD | a JSON number |
| Boolean | `true` / `false` |
| Date | `"2026-12-24"` |
| Time | `"14:30:00"` |
| Date/time, timestamp | `"2026-12-24T14:30:00.000"` |
| NULL | `null` |
| Text, memo | a JSON string |
| Binary blob | `"(binary, 4096 bytes)"` |

Deliberately **not** `AsString` throughout: a caller that read a record and
wrote it back would otherwise have to convert numbers and dates by hand, and
differently depending on the Windows locale of the machine it ran on. Date
and time are local and carry no zone designator — a date field has no time
zone, and neither does the value on screen.

Binary blobs are the one exception. `AsString` would put raw bytes into the
JSON and Base64 would put a whole picture into every row of a listing, so
they are named and measured instead. Memo fields are text and are delivered
as text.

Going the other way, an unknown or non-writable field raises rather than
losing the value silently, and a date that is not ISO 8601 is refused rather
than guessed at. If a `Post` fails, the editing state is rolled back —
otherwise the dataset would be stuck in `dsEdit` as far as the application
is concerned.

A listing reports `rowCount` and `truncated`. Both come from walking the
rows, not from `RecordCount`, which several drivers answer with -1 or only
after fetching everything.

**When the application asks back:** if a confirmation dialog hangs off
`BeforeDelete`, the call sits there until it is answered. That is what
`/dialogs` is for — arm the answer **first**, then `op=delete` runs through
and the logic in the handler runs with it, checks and cleanup included:

```
POST /dialogs?answer=yes&count=1
POST /dataset?name=tblData&op=delete
```

This only works if `DialogFormFilter` names your dialog class, see
[Answering message dialogs](#answering-message-dialogs).

### Errors instead of crashes

When an operation fails, the exception is caught and reported — with
**class and message kept apart**, so a caller can test for
`EDatabaseError` without searching through prose:

```json
{"ok":false,"error":"Dataset delete: operation aborted",
 "exceptionClass":"EAbort","exceptionMessage":"Operation aborted"}
```

With `onerror=raise` the exception is **additionally** handed to
`Application.HandleException`, so the application behaves as it otherwise
would — meant for testing exactly that behaviour. This exists only where the
operation runs on the main thread; the file endpoints deliberately run
beside it and always just report.

---

## Files and directories

Requires **two** deliberate steps: `FileAccess` set to `acFull` (the default
is `acDenied`) **and** a `FileRoot`. One of the two is the one you forget.

```pascal
RestAgent.FileAccess := acFull;
RestAgent.FileRoot := 'testdata';   // relative to the program directory
```

`FileRoot` must lie in the **program directory or below**. A relative value
is resolved against that directory, not against the current working
directory — the latter wanders as soon as a file dialog has been open once.
`.` means the program directory itself — which exposes everything sitting
next to the executable, settings and all. That is a decision to make
deliberately, not by default.

| Call | Effect |
| --- | --- |
| `GET /files?path=&pattern=*.log` | List a directory |
| `GET /file?path=notes.txt&encoding=ansi` | Read |
| `POST /file?path=notes.txt&encoding=ansi` | Write, content in the body |
| `POST /file?path=run.log&append=true` | Append |
| `DELETE /file?path=notes.txt` | Delete |
| `GET /directory?path=sub` | List |
| `POST /directory?path=a/b/c` | Create, intermediate levels included |
| `DELETE /directory?path=a&recursive=true` | Delete; without `recursive` only an empty one |
| `POST /rename?path=old.txt&to=new.txt` | Rename, file or directory |

`encoding` is `utf8` (default), `ansi` or `base64`.

Text is written without a byte order mark. On reading, one present in the
file is stripped so it does not show up as a visible character.

What gets rejected: `..` in any spelling, a drive letter of its own, a UNC
path, the leading separator, and the reserved device names (`NUL`, `CON`,
`COM1` …), which Windows resolves in *every* directory — `logs\NUL` passes
every text comparison with the root and still writes into nothing. The
comparison uses the **resolved** path, not the text as given — otherwise
`sub/../..` would slip through. The target of `/rename` goes through the
same check, because renaming would otherwise be the comfortable way out of
the exposed directory. Directories are never deleted recursively unless
asked, and the exposed directory itself never is.

`MaxFileBytes` (default 1 MB) applies to reading and writing alike. On
reading only that many bytes are fetched — reported through `truncated`, not
silently, with `size` still naming the real length. On writing, the limit is
measured against **what the file ends up as**, appends included, and
anything larger is rejected with **413**.

---

## Recording exceptions

`LogExceptions := True` records the application's exceptions in a ring
buffer, available through `GET /exceptions`. `DELETE` empties it and reports
in `cleared` how many entries were discarded — whoever tidies up before a
test run should be able to tell whether they threw away something they
should have read. That is also why `DELETE` counts as changing something:
under `ReadOnly` it answers 405.

> **The default is off, and deliberately so.** Enabling it takes over
> `Application.OnException`. A handler already installed there is called
> after the entry is recorded, so the chain stays intact. Whether a tool
> hooked in further down — **madExcept**, for instance — remains unaffected
> depends on where it hooks, and cannot be guaranteed from here. If you
> depend on crash reports, switch this on only after a deliberately provoked
> error has shown you that the report still arrives.

---

## Environment and time

| Path | Purpose |
| --- | --- |
| `/nop` | Answers 200 and nothing else. Unlike `/ping` it does not even reveal the application name. |
| `/time` | Local time and UTC, offset, time zone, daylight saving, tick count since boot. No setting. |
| `/machine` | Processor, cores, memory, operating system, monitors with position and scaling, network adapters with MAC and addresses. |

The tick count from `/time` is more dependable for measuring intervals than
the clock, which can be adjusted. The scaling from `/machine` explains why
coordinates taken from a screenshot do not match those of the interface.

> **`/nop` answers even when the application is busy.** It is one of the two
> endpoints that do not go to the main thread at all (`/openapi.json` is the
> other), so it still replies while a modal dialog blocks everything else.
> That is what makes it worth having: when `/ping` hangs and `/nop` answers,
> the service is alive and the *application* is the one that is stuck — a
> distinction you cannot otherwise make from outside. What is blocking it,
> though, the interface cannot tell you.

---

## Shutdown and restart

Both behind `LifecycleAccess`, **denied** by default.

| Call | Effect |
| --- | --- |
| `POST /terminate` | `Application.Terminate` |
| `POST /restart` | Shut down and start again |
| `&force=true` | Bypasses `FormCloseQuery`; unsaved work is then gone |
| `&delay=500` | Delay in milliseconds, at least 100 |

Execution runs delayed through a timer and answers **202** — otherwise the
application would be gone before the answer reached the caller. The restart
goes through the command processor with a wait: were the new instance to
start immediately, it would find the port still taken.

---

## Self-description: OpenAPI

```
GET /openapi.json
```

returns an OpenAPI 3.0.3 document that is **generated, not shipped**. That
is the difference that matters: it names the actual address, the configured
base path, the authentication scheme in force, and — per endpoint — whether
that area is open in *this* installation at all.

What is denied is **listed and marked deprecated** rather than omitted: a
caller should be able to tell "does not exist here" from "is closed here".
Two extra fields carry the detail:

| Field | Meaning |
| --- | --- |
| `x-vcl-area` | Which area the endpoint belongs to |
| `x-vcl-access` | The level **actually** in force here, `ReadOnly` already folded in |

A client — and an AI client in particular — asks once and knows what it may
attempt, instead of walking into a wall of 403s:

```
curl -s http://127.0.0.1:8377/openapi.json
```

With `FileAccess=denied` and `ReadOnly=1`, for instance, the description
reports `POST /property` as `acReadOnly` and every file path as `acDenied`,
while `GET /property` stays open.

**Where it stops:** paths, methods, parameters, security and the response
envelope are covered. The fields below `data` differ per endpoint and are
**not** described there — that is what this document is for.

The description is generated from the table in the code
(`TVclRestEndpointDoc`, `class function Endpoints`) that also carries each
endpoint's area and its methods — the same table the dispatcher checks every
request against. Not a second file that gets forgotten the next time an
endpoint is added. `Endpoints` is public, along with `AreaOf`,
`ChangesApplication`, `MethodsOf` and `MethodAllowed`; anyone wanting to
generate something else from it can.

The file `VclRestAgent.openapi.json` next to this readme is the
version pulled from a running application with permissions open.

---

## Application-specific endpoints

Anything the component cannot know generically — a query through the
application's own database layer, say — goes through `OnCustomRequest`.
Every path under `/custom/` is handed there:

```pascal
procedure TdmMain.RestAgentCustomRequest(
  Sender: TObject;
  const AMethod, APath: string;
  AParams: TStrings;
  const ABody: string;
  var AResponse: string;
  var AStatusCode: Integer;
  var AHandled: Boolean);
begin
  if LowerCase(APath) = 'sql' then
  begin
    AResponse := RunQueryAsJson(AParams.Values['sql']);
    AHandled := True;
  end;
end;
```

`APath` arrives already stripped of the prefix (`/custom/sql` → `sql`). Set
`AResponse` to JSON text, optionally `AStatusCode`, and switch `AHandled` to
`True`. Leave it `False` and the component answers 404 by itself.

The handler runs **on the main thread already**, so it may touch datasets
and the user interface directly. It also sees every method — what exists
below `/custom/` only the application knows, so the method check does not
apply there.

Obvious uses beyond a query: reporting connection state, seeding a fixture
before a test, resetting to a known state afterwards.

> `/custom/` is reachable or not — `CustomAccess` decides that much — but
> `ReadOnly` does not reach inside your handler, because the component
> cannot judge what happens in it. If yours changes anything, ask
> `TVclRestAgent(Sender).ReadOnly` before doing it.

---

## Calling without the network: `Execute`

```pascal
function Execute(
  AMethod: TVclRestMethod;
  const ARequestUrl: string;
  const ARequestBody: string;
  out AResponseCode: Integer;
  out AResponseBody: string): Boolean;
```

Processes a request directly, without the built-in server — for when the
network connection terminates somewhere else entirely (your own server, a
message channel, test code), and only the evaluation should happen here.

```pascal
var
  Code: Integer;
  Body: string;
begin
  if RestAgent.Execute(rmGet, '/actions', '', Code, Body) then
    Memo1.Text := Body;
end;
```

`ARequestUrl` may be the full form
(`http://host:8377/api/focus?name=x`) or just the path
(`/api/focus?name=x`). The result is `False` only for an unusable URL or a
path outside `BasePath` — the caller can then tell "not my request" from "my
request, unknown endpoint". An error status is still `True`: the request was
processed, the answer simply says no.

`TVclRestMethod` covers `rmGet`, `rmPost`, `rmPut`, `rmDelete`, `rmHead`
and `rmOptions`, so everything the built-in server can answer is reachable
this way too.

`Execute` works whether or not `Active` is `True`, and it does **not**
check authentication — the call comes from inside the application.

This is also what the test suite drives: the whole chain from the URL to the
handler and back, without a socket and without a port.

---

## Threading

Indy serves each request on its own thread, but the VCL is not thread-safe.
Every access to forms, controls, actions or datasets is therefore marshalled
onto the main thread; exceptions raised there are caught and re-raised on
the server thread, so they reach the caller as an HTTP 500 with a message
instead of disappearing.

A side effect that is deliberate: if the application is busy, the request
waits. The answer then reflects the real state rather than a convenient
fiction. The flip side is that the component cannot inspect an application
that has hung.

Three exceptions, all on purpose: the file endpoints do not touch the user
interface, and a large file would otherwise stall the main thread for the
duration of the read; `/openapi.json` reads settings and builds text, so
marshalling the one call a client makes first would block the application
for nothing; and `/nop` touches nothing at all, which is what lets it
answer while everything else waits.

---

## Example: an automated sequence

```bash
BASE=http://127.0.0.1:8377

# Is anyone there, and may I do what I am planning?
curl -s $BASE/nop
curl -s $BASE/openapi.json | jq '.paths."/action".post."x-vcl-access"'

# Navigate to an article and check what is on screen
curl -s -X POST "$BASE/dataset?name=tblData&op=locate" \
     -H "Content-Type: application/json" -d '{"article_no":"4711"}'
curl -s "$BASE/dataset?name=tblData&at=current"

# Trigger an action that opens a dialog, and answer it in advance
curl -s -X POST "$BASE/dialogs?answer=yes&count=1"
curl -s -X POST "$BASE/action?name=actSave&async=1"

# Wait for the outcome instead of sleeping
curl -s "$BASE/wait?idle=1&timeout=5000"

# Read back what actually happened
curl -s "$BASE/property?name=edtTotal&prop=Text"
curl -s "$BASE/dialogs"
curl -s "$BASE/exceptions"
```

Every step is a verified fact. Nothing here is inferred from a picture, and
nothing depends on a sleep being long enough.

---

## Built with Kai

This component was built with **Kai**, the AI integration in RAD Studio.
That is worth a paragraph here, because it shaped the result.

What changes when the assistant sits inside the IDE is not that code gets
written faster. It is that the loop closes: write, compile, read the real
compiler message, fix, compile again, without a human carrying text between
two windows. The 292 tests in `Tests\VclRestAgentTests.dproj` run after
every change because running them costs nothing — and that is the only
reason a review of this size was worth doing. Half the defects it turned up
would have been invisible to reading alone.

There is a second reason the two belong together. Kai gives an AI a grip on
the **source**: editor, compiler, project. What it cannot give it is a grip
on the **running program** — whether the button it just wired up is enabled,
whether the value in that field was committed, whether the screen has
finished rebuilding. That is the gap this component fills: Kai writes the
code, `TVclRestAgent` lets the same assistant check that the running
application does what the code says.

---

## Limits

- **VCL only.** It reads the VCL component tree.
  `Application.MessageBox` is a plain Win32 window without a VCL form and
  therefore out of reach; so is anything drawn by a control that keeps its
  state to itself — a grid that holds its cells in a data controller of its
  own shows you the underlying dataset through `/dataset`, not its cells.
- **No menus.** There is no endpoint for picking a menu item. A menu item
  backed by an action is reachable through `/action`, which is the usual
  case in a grown application; anything else is not. A generic VCL menu
  endpoint would have been possible but would stay blind for the
  third-party toolbars and ribbons that most real applications use, and
  half a promise is worse than a named limit.
- **Setting is not typing** (see above), and sending to an element is not
  typing either — see [Typing](#typing-two-ways-in-and-the-difference-matters).
- **It waits with the application** (see above). `/nop` still answers, so
  you can tell a stuck application from a dead service — but not what is
  blocking it.

> **Not for production.** This is a testing and diagnostics tool. It can
> drive the application and read its data, and it is not built to withstand
> an attacker. Binding it to anything other than the loopback address
> belongs in a sealed-off test environment — nowhere else. On a production
> machine it should not be enabled at all.
