pyrtan / sourcev0.13.0 · main

pyrtan / _meta / io.md

8,553 bytes · last changed 2026-10-26 in Watches and growing from a gap: trees that run when something speaks, and the branch a gap asks for drafted by rules; release 0.9.0

Design — Forms of I/O: file, shell, serial, queue, browser

StatusBuilt, v0.7. Five sources beside http, each a node table with an allow-list of its own kind, the same retry rule, the same trace, the same fixtures; the examples cover each; the JavaScript runtime refuses them on a member and answers them from a fixture.
Date2026-10-25
DecisionA node names a *source* and what it wants from it; the tree lists what that source may reach; the runtime refuses anything outside the list before the adapter is asked; the adapter moves the bytes and leaves a plain table in state. Files, shells and serial lines are the box itself, and a hosted service keeps them closed.
Wheresrc/pyrtan/adapters.py (the adapters, the allow-list matchers, the fixture keys), src/pyrtan/tree.py (the lists, Tree.allowed, the per-source checks), src/pyrtan/runtime.py (one path for every source), src/pyrtan/transport.py (fixtures answer for every source), src/pyrtan/js/pyrtan.js (the same on a member).

0. The brief

"Act on all forms of I/O." v0.1 spoke HTTP and said the shape would hold for any other form: an adapter names what it may reach, a node observes or acts through it, a decide reads what came back. v0.7 is that sentence made true five times, and nothing more: no new node kind, no new template syntax, no way to reach anything a tree did not list.

1. The shape, once

An observe or act node has exactly one of http, browser, file, shell, serial, queue, from or emit. The runtime renders the source's table with the node's scope, asks the tree whether the rendered request is inside its allow-list, and only then asks the adapter. An observe retries on a transport error (not on an answer the author can decide on); an act is attempted once; a policy refusal is never retried. The result is a table under into, extract reads it (every source leaves body, so regex and css extracts work unchanged), and the trace line reads source what -> brief, the way an HTTP line reads GET url -> status.

SourceAllow-listWhat it namesWhat comes back
httphostsa URLstatus, ok, headers, body, json, url, elapsed_ms
browserhostsa URL, wait_for, actionsurl (where it ended up), status, ok, title, body (rendered), elapsed_ms
filepathsa path, tail; write or appendpath, exists, size, modified, body, json, lines; written
shellcommandsa program and argscommand, args, status (exit code), ok, stdout, stderr, body, json, lines
serialdevicesa device, baud, write, read_until or readdevice, baud, sent, body, bytes, complete, json, lines
queuehosts and topicsa broker URL and a topic; publishreceived, message (topic, body, json), body, json; published

2. The allow-lists

Each list is checked twice, like hosts: at load time when the value is literal (pyrtan check refuses shell = { command = "rm" } in a tree whose commands is ["df"]), and at run time when it is templated (paths = ["/var/log"] refuses /etc/passwd from a sentence, and /var/log/../etc/passwd, because the path is made absolute first). A tree that uses a source without its list does not load. The matchers:

3. What this design refuses

4. Fixtures

One fixture file answers for every source. An HTTP record is keyed GET url; the others by adapter_key: shell df -P /, file /var/log/syslog (or file append …), serial /dev/ttyUSB0 "AT\r\n", queue mqtt://broker home/kitchen/temperature (or queue publish …), browser https://…. The record is the table the node would observe, or {"error": "…"} for a transport failure. pyrtan run --record writes them from real runs; the JavaScript runtime builds the same keys, so tests/fixtures/ examples.json serves both runtimes and the vectors in tests/conformance cover a shell, a file, a queue, a browser and a serial line without any of them being present.

5. Failure modes, honestly

6. Sequencing