/*{ "parent": "utilities", "description": "EXPERIMENTAL agent interface: expose a tosijs app's state, wiring, and actions to AI agents (and test harnesses) as a described, observable, path-addressed surface." }*/ /*# # agent (EXPERIMENTAL) `enableAgentInterface()` turns a tosijs app's existing records — the state registry, the binding wiring, the event handlers — into a described, path-addressed surface for *non-human users*: AI agents, test harnesses, automation. Nothing is recorded that tosijs doesn't already know; `describe()` assembles the picture on demand. **Nothing is exposed until you say so.** Say what an agent may see, and that is exactly what it sees: import { tosi, enableAgentInterface } from 'tosijs' const { app } = tosi({ app: { cart: [], filter: '', addItem() {} } }) const agent = enableAgentInterface({ expose: { roots: [app.cart, app.filter], actions: [app.addItem, app.checkout], write: true, // omit for scoped reads with no writes }, }) agent.describe() // roots, wiring (elements ↔ paths ↔ handlers), actions agent.read(app.filter) // serializable value agent.observe(app.cart, (path) => { ... }) // push; returns un-observe agent.changes(cursor) // turn-based drain: final value per changed path await agent.when(app.order.status, (s) => s === 'confirmed') // await a condition agent.write(app.filter, 'milk') // through the same observers as any write agent.call(app.addItem, 'buy milk') // invoke an action by path agent.log() // the audit trail **Paths or proxies, everywhere.** Every verb and every manifest entry takes either — `agent.read(app.filter)` and `agent.read('app.filter')` are the same call, because the proxy already carries its path. Prefer the proxy: it survives a rename and it cannot be misspelled. Strings remain fully supported, and are what you want when the path arrives from outside the program (a tool call, a config file, a wire message). Anything that is *neither* — a plain object, a raw value read out of the tree — is **refused**, with `kind: 'path'`. It used to be coerced with `String()`, so `roots: [app.cart]` declared a root named `"[object Object]"` and every read then failed as out-of-scope: a broken manifest whose error blamed the reader. Worse for a scalar, where `String()` yields the *value*, so `read(app.filter)` quietly read whatever path the filter text happened to spell. Undeclared state is not redacted, it is **absent**: it never enters the map, the elements bound to it never appear in `wiring`, and every verb refuses the path. A manifest scopes **sight**, not reach — `roots` says what may be seen, `write: true` is a separate grant to change it, and declared `actions` stay callable either way. `describe().writable` reports which you have. While developing, one word opens everything: const dev = enableAgentInterface({ expose: 'all' }) // and warns that it did `enableAgentInterface()` with no manifest is legal and **exposes nothing** — `describe()` reports an empty app and every verb refuses. That is deliberate: the default used to be read-only over the *entire registry*, which is how four separate secret leaks became reachable through one unargumented call. Scope is the control; the redaction described below is defence in depth beneath it. ## Secrets > ⚠️ **`data-tosi-secret` is badly named and the name will change.** It marks > what *this library* must not copy into a description. It is **not a security > boundary and cannot be one**: everything it covers is ordinary client-side > DOM, readable with one `querySelector` by any script in your origin, any > extension, and anyone with devtools — whatever tosijs does. > > Worse, **it is an attribute, so it is a signpost.** > `document.querySelectorAll('[data-tosi-secret]')` hands a reader your own > curation of which parts of the page you considered sensitive. That judgement > is work an attacker would otherwise have to do. Nothing here is disclosed > that was not already reachable — but the marker tells you where to look. > > So: **mark as little as possible, and do not reach for it as protection.** > The actual control is `expose` — what the surface is willing to describe at > all — and the CLOSED default beneath it. This mechanism is defence in depth > for the case where a description **leaves the origin** (a model-context host > receiving `tosi_describe`); against anything running *in* the page it buys > you nothing. A rename, and a way to declare withholding in code rather than > in markup, are planned for 1.12.0. A path bound to a password field, a `cc-*` autocomplete, a hidden CSRF token, or anything you mark `data-tosi-secret` is withheld — it reads back as the sentinel `⟨secret⟩` rather than its value: agent.read('app.login.password') // '⟨secret⟩' agent.read('app.login') // { user: 'ada', password: '⟨secret⟩' } This matters for what you DID expose — an undeclared path is absent, not redacted, so redaction is what protects a secret sitting *inside* a declared root (and it is the only thing protecting you under `expose: 'all'`). **And `describe()` withholds the element's own content, not just its bound value.** A secret-marked element — or any element inside a `data-tosi-secret` region — publishes its tag, role, name, bound path and geometry, but *not* its `href`, `placeholder`, `title`/`alt`-derived name, `aria-description` or a toggle's `checked` state. A reset-link token lives in an `href`, not in a bound path, and until 1.10.2 it travelled in cleartext beside a `text` field that had been correctly withheld. The record carries `secret: true` so a consumer can tell suppression from absence. An **`aria-label` survives** — it is authored to be announced, and dropping it would make every secret control anonymous to assistive tech and to the audit. Names survive secrecy; content and live state do not. **Secrecy is a property of the PATH, not of an element.** Marking one control secret withholds that path everywhere it surfaces — `read`, `describe`, `changes`, `when` — including from *other* elements bound to the same path, and from every field beneath it if the path names an object. It is also one-way for the session: a path that was ever secret stays secret, because the alternative is a window in which it isn't. > ⚠️ **The path has to be LEARNED first, and that is where the gap is.** > tosijs discovers a secret path by finding a secret control and looking for > the binding that feeds it: on the control, on its immediate parent, through > a wrapping `