385 lines
22 KiB
Markdown
385 lines
22 KiB
Markdown
# Runa Protocol
|
|
|
|
`runa-protocol` defines versioned request, response, and event data contracts
|
|
for clients and the `runa` dispatcher. It is independent of transport
|
|
and runtime implementation. `runa` accepts WebSocket text messages bound to an IPv4 or
|
|
IPv6 loopback address, defaulting to `127.0.0.1:3737` without arguments.
|
|
Clap parses `--listen` / `RUNA_LISTEN` on the server and `--connect` /
|
|
`RUNA_CONNECT` on `runa-cli`; explicit flags override environment variables.
|
|
The listener is unauthenticated and intended only for
|
|
trusted local development. Both the REPL and VS Code use WebSockets.
|
|
|
|
## Version 5
|
|
|
|
Every request and response carries `version: 5`. Requests also carry a
|
|
non-empty `request_id` used to correlate the response. A server must reject
|
|
unsupported versions with `unsupported_version` rather than interpreting a
|
|
request using a different contract. Events carry the protocol version and do
|
|
not have a request ID.
|
|
|
|
Requests have a tagged command object:
|
|
|
|
```json
|
|
{
|
|
"version": 5,
|
|
"request_id": "req-1",
|
|
"command": {
|
|
"operation": "send",
|
|
"parameters": {
|
|
"actor_ref": "xK8_pQ2-Lm5_012345678",
|
|
"message": {
|
|
"type": "record",
|
|
"data": {
|
|
"op": { "type": "string", "data": "send" },
|
|
"value": { "type": "integer", "data": 9 },
|
|
"target": { "type": "actor_ref", "data": "Hj2_9sM0-pQ4_7aBcD3eF" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The version 5 operations are source listing/read/write, commit/history/revision
|
|
retrieval/rollback, actor spawn, send, tick, inspect, actor listing, event
|
|
retrieval, privileged system messages, and cancellation.
|
|
Migration closures are intentionally excluded: they are host callbacks, not
|
|
portable wire values.
|
|
|
|
Source paths are extensionless relative paths such as `actors/counter`, not
|
|
behavior aliases. They exclude the `source/` root and `.rn` extension.
|
|
|
|
| Operation | Parameters | Reply kind/data |
|
|
| --- | --- | --- |
|
|
| `save_image` | `file`: new server-local destination | `acknowledged` |
|
|
| `restore_image` | `file`: server-local snapshot to replace image | `acknowledged` |
|
|
| `list_sources` | none | `sources`: file array |
|
|
| `read_source` | `path` | `source`: file |
|
|
| `check_source` | `source` (including unsaved text) | `source_checked`: `{diagnostics}` |
|
|
| `write_source` | `path`, `expected_source_id` (null to create), `source` | `source`: file |
|
|
| `rename_source` | `path`, `target`, `expected_source_id`, `update_actors` | `source`: renamed file |
|
|
| `delete_source` | `path`, `expected_source_id` | `acknowledged` |
|
|
| `actor_behavior` | `actor_ref` | `actor_behavior`: `{source_id, source}` |
|
|
| `write_actor_behavior` | `actor_ref`, `expected_source_id`, `source` | `actor_behavior`: `{source_id, source}` |
|
|
| `update_behavior_source` | `actor_ref`, `path`, `expected_source_id`, `temporary_source_id` | `source`: updated original file |
|
|
| `commit_source` | `path` | `source_committed`: commit |
|
|
| `source_history` | `path` | `source_history`: commit array |
|
|
| `revision_source` | `path`, `revision` | `behavior_source`: `{source}` |
|
|
| `rollback_source` | `path`, `revision` | `source`: restored file |
|
|
| `spawn` | `path`, tagged `state`, positive `mailbox_capacity` | `actor_spawned`: `{actor_ref}` |
|
|
| `inspect_execution` | `actor_ref` | `execution_inspected`: loaded source and optional execution stack |
|
|
| `set_breakpoints` | `actor_ref`, `implementation_id`, `lines` | `breakpoints_set`: `{breakpoints}` |
|
|
| `step_source` | `actor_ref`, `implementation_id`, `instruction_budget`, optional `mode` | `debug_step_completed`: `{reason}` |
|
|
| `system` | `actor_ref`, `message` | `acknowledged` |
|
|
|
|
`save_image` and `restore_image` are additive v5 operations. File paths must be
|
|
nonempty, contain no NUL and fit 4096 UTF-8 bytes. Snapshot contents are stored
|
|
on the server, not transferred through the 1 MiB WebSocket envelope.
|
|
Save refuses existing destinations and unfinished handlers, and leaves runnable
|
|
actors paused on success. Restore validates and recompiles the entire snapshot
|
|
before replacing an image that has no unfinished handlers. Saved runnable actors
|
|
resume eligibility; previously paused/failed actors retain their status.
|
|
Incarnations advance to invalidate old debugger sessions. Both successful
|
|
operations emit the normal coalescible `image_changed` notification.
|
|
File/validation failures are explicit protocol errors; neither operation is
|
|
retried automatically. See [persistence semantics](runtime.md#image-save-and-restore).
|
|
These privileged commands grant server-local file access and must be used only
|
|
on a trusted machine, just like code installation.
|
|
|
|
A file is `{path, source_id, source}` and a commit is
|
|
`{revision, source_id}`. Commits generate readable random revision strings and
|
|
capture working text without compilation. Rollback restores working source
|
|
without appending history. Spawn compiles current working source even when
|
|
uncommitted; compilation failures do not spawn an actor.
|
|
|
|
Rename preserves history and atomically updates installed/pending actor path
|
|
bindings when `update_actors` is true. In-use paths require that authorization;
|
|
existing destination paths are never overwritten. Delete discards working text
|
|
and history and rejects installed/pending users unless all are failed, with an
|
|
explicit stop-first error. Compiled artifacts are retained.
|
|
|
|
`actor_behavior` reads the existing temporary text, or the actor's loaded
|
|
artifact rather than its current working file. Writing compiles first and
|
|
schedules an actor-local safe-boundary install, retaining state/mailbox and
|
|
leaving shared source/history untouched. A failed actor's implementation may be
|
|
replaced without restarting or acknowledging its failed input. Reload/become
|
|
clear temporary text after successful scheduling; restart retains it; removal
|
|
cleans it up. `update_behavior_source` copies temporary text into shared working
|
|
source without committing. Its original path and both source identities are
|
|
checked before writing. Stale source identities/path bindings return `conflict`.
|
|
|
|
Actor snapshots add `behavior_path` (nullable installed source path),
|
|
`pending_behavior_path` (nullable scheduled source path), and
|
|
`temporary_behavior` (boolean). These are additive v5 metadata fields; older
|
|
v5 snapshots may omit them and clients default to null/false.
|
|
|
|
### Compilation diagnostics
|
|
|
|
`check_source` compiles a standalone text buffer with the image's native modules.
|
|
It does not execute, save, commit, cache, install, or emit `image_changed`.
|
|
Both invalid and valid source return `source_checked`: compiler errors/warnings
|
|
are validation results; infrastructure failures still use the error envelope.
|
|
|
|
Each diagnostic is `{severity: "error"|"warning", message, range}`. A non-null
|
|
range is `{start: {line, character}, end: {line, character}}`, with zero-based
|
|
lines, UTF-16 code-unit columns, and an exclusive end. Ranges refer to the exact
|
|
submitted text, not the current contents of a source path. `range: null` denotes
|
|
an error without a location in the submitted source. There are no synthetic
|
|
locations inferred from compiler display strings.
|
|
|
|
Compilation failures from spawn/reload/become/temporary behavior writes also
|
|
carry an optional `diagnostics` array in `ProtocolError`, retaining the existing
|
|
`runtime_failure` code and human-readable `message`. This is an additive v5
|
|
field: absent diagnostics deserialize as an empty list and are omitted when
|
|
empty. Source checking is single-file; it does not resolve other image files
|
|
as modules.
|
|
|
|
### Execution inspection
|
|
|
|
`inspect_execution` is an additive, read-only v5 operation. It returns
|
|
`{actor_ref, incarnation, implementation_id, source_id, source, behavior_path,
|
|
execution, debugger}`. Source is the exact original text of the installed artifact, not
|
|
working source or a pending temporary replacement. The nullable behavior path
|
|
is a logical binding, not a guarantee that a mutable file still matches the code.
|
|
Idle/failed actors return `execution: null`; removed actors return `not_found`.
|
|
Inspection does not tick, mutate the image, consume events, or emit `image_changed`.
|
|
|
|
A non-null execution is `{vm_steps, awaiting, call_depth, frames,
|
|
frames_truncated}`. Frames are ordered innermost first, limited to 64 including
|
|
the current frame; `call_depth` reports the full number of saved callers.
|
|
Each frame is `{kind: "current"|"caller", instruction_pointer,
|
|
source_instruction_pointer, function, range}`. Current IP is the next instruction
|
|
at a completed step boundary; while awaiting, it is the await instruction from
|
|
the last safe snapshot, not an IP observed inside a borrowed pending future.
|
|
Caller IPs are saved return addresses; source IPs identify the preceding call
|
|
instruction. Source IP, function, and range may be null when debug information
|
|
is unavailable. Ranges use the same zero-based UTF-16/exclusive-end coordinates
|
|
as diagnostics and refer only to the returned loaded source.
|
|
|
|
Only the head Rune VM's visible frames are captured, not native stacks or stacks
|
|
hidden inside nested futures. Pending async steps are not counted as completed
|
|
VM steps. Snapshots contain no VM stack values or mutable execution handles.
|
|
There is no locals inspection, post-mortem stack retention, or DAP contract.
|
|
Responses are point-in-time; clients must pause explicitly for stable inspection.
|
|
|
|
### Source debugger controls
|
|
|
|
These additive v5 mutations are available to trusted clients and broadcast
|
|
`image_changed`. Both require the expected `implementation_id` display string;
|
|
a stale installed identity returns `conflict` without mutation. Pending shared
|
|
or temporary source replacements do not retarget the controls.
|
|
|
|
`set_breakpoints` replaces the selected actor's list with up to 128 zero-based
|
|
32-bit line numbers; `[]` clears it. Returned breakpoints are sorted/deduplicated
|
|
`{line, instruction_pointers}` entries. Addresses identify mapped line entries
|
|
(line transitions, function entries, or labeled branch targets); an empty list
|
|
means the requested line is unverified. No relocation to another line occurs.
|
|
During ticks, a hit pauses that actor before execution. Resume skips that
|
|
stopped address once, not all subsequent hits.
|
|
|
|
`step_source` requires a paused actor with an active handler/shutdown invocation,
|
|
and a positive budget no greater than 10,000. It advances only that actor, stopping
|
|
according to the selected mode, another breakpoint,
|
|
pending async operation, completion/failure/removal, or budget exhaustion.
|
|
`reason` is one of `step`, `breakpoint`, `budget`, `awaiting`, `completed`,
|
|
`failed`, `removed`. Failure is an executed actor transition with explicit reason
|
|
and failure text in its ordinary snapshot, not an infrastructure success fallback.
|
|
Nonfailed surviving actors remain paused, including at a completed handler
|
|
boundary; drafts/sends commit only on normal completion. Another explicit step
|
|
may poll a pending async operation once; steps do not block waiting for it.
|
|
Native calls remain cooperative, not preemptible.
|
|
|
|
`mode` is `into` (default when absent), `over`, or `out`; other values are rejected.
|
|
Into stops at another mapped line or call-depth change. Over skips deeper calls
|
|
and stops on another mapped line at the original depth or any mapped shallower
|
|
frame. Out stops at a mapped shallower frame, or completes the invocation when
|
|
started at the outermost frame. Nested breakpoints interrupt any mode. Budgets,
|
|
pending awaits, failure and removal retain their usual stop semantics. Each
|
|
request uses the current frame as its origin; no pending over/out plan survives
|
|
a budget/await/breakpoint stop. Omitted-mode requests keep their existing into
|
|
behavior. Over/out require a server implementing this additive mode field.
|
|
|
|
`execution_inspected` adds `debugger: {breakpoints, stop_reason}` even when
|
|
`execution` is null. Older replies may omit it; Rust defaults to an empty
|
|
configuration and no reason. `stop_reason` is nullable and uses the same reason
|
|
strings as step replies. Installation clears breakpoints; restart/cancel retain
|
|
them for unchanged loaded code and clear stop reasons. Debugger state is actor
|
|
local, shared by clients, and in-memory; it is not owned by a client connection.
|
|
There are no conditions, hit counts, or gutter/DAP breakpoints yet.
|
|
|
|
System message shape is `{kind: "stop"|"kill"|"restart"|"pause"|"resume"|"reload"}`
|
|
or `{kind: "become", path: "actors/other"}`. This is a trusted host request;
|
|
actor `ctx.send` and `ctx.send_from` accept only ordinary data. Stop runs optional
|
|
`on_shutdown` after the active handler and removes the actor on success. Kill
|
|
removes the actor immediately without shutdown, including failed actors. Removed
|
|
references return `not_found`; state, mailbox, and retained logs are discarded.
|
|
Failed actors are not automatically removed. A `Stopped` event denotes removal,
|
|
and `image_changed` invalidates actor lists. Restart is independent of stop/kill and
|
|
retains identity, mailbox, the loaded script and state; removed actors cannot be
|
|
restarted. Reload/become defer installation to
|
|
the next handler boundary. Normal scheduling advances through `tick`; the
|
|
explicit debugger step operation advances only its selected paused actor.
|
|
|
|
Responses echo the request ID and carry either a typed reply or a protocol
|
|
error. Events use a separate versioned envelope. Actor and source snapshots
|
|
are wire DTOs, not serialized runtime objects; implementation identities are
|
|
represented by their stable display strings. Public actor identity is a
|
|
21-character URL-safe NanoID string carried in `actor_ref`. The server maps known service
|
|
failures to the defined error codes and otherwise uses `runtime_failure`
|
|
rather than claiming a more specific category.
|
|
|
|
### Portable values
|
|
|
|
Spawn state, send messages, snapshot state/mailbox/reservations, and migration
|
|
event states use the same tagged value shape:
|
|
|
|
| Value | JSON |
|
|
| --- | --- |
|
|
| Null | `{"type":"null"}` |
|
|
| Boolean | `{"type":"bool","data":true}` |
|
|
| Signed 64-bit integer | `{"type":"integer","data":42}` |
|
|
| String | `{"type":"string","data":"hello"}` |
|
|
| List | `{"type":"list","data":[<value>, ...]}` |
|
|
| Record | `{"type":"record","data":{"key":<value>, ...}}` |
|
|
| Actor reference | `{"type":"actor_ref","data":"<21-character-NanoID>"}` |
|
|
|
|
References are distinct from strings. Their shape is validated even when
|
|
nested; existence is checked when a reference is used as a send destination,
|
|
not merely stored as data. The old message envelope and protocol v2 are not
|
|
accepted. The REPL's natural JSON and `$actor`/`$record` conveniences are
|
|
client syntax, not wire encodings.
|
|
|
|
Each state/message permits a maximum nesting depth of 32 (root depth 0),
|
|
4,096 nodes, and 1 MiB of UTF-8 string/key bytes. Requests are also subject to
|
|
the transport's 1 MiB total message limit, including tags and envelope.
|
|
Unsupported value tags, malformed references, floats, and out-of-range integers
|
|
fail decoding; well-formed requests exceeding value limits receive
|
|
`invalid_request`. Invalid Rune transition output fails the actor without
|
|
acknowledging the reserved message, changing committed state, or enqueueing
|
|
any of its sends.
|
|
|
|
`get_events` drains and returns the events currently accumulated by this
|
|
single-worker image. It is not a durable subscription or replayable event log.
|
|
The transport limits each request to 1 MiB before parsing. Malformed
|
|
JSON or an oversized message terminates that connection with an error; no protocol
|
|
response is emitted when a request cannot be decoded. A WebSocket disconnect
|
|
closes only that client connection. The listener
|
|
allows up to 16 simultaneous clients and serializes each dispatch through the
|
|
same image. WebSocket response payloads are capped at 1 MiB, with one
|
|
in-flight dispatch per client and a bounded shared dispatch queue. Every local client can use the full development
|
|
API; loopback binding is not authentication or authorization. Remote access
|
|
must not be enabled until appropriate authentication and authorization exist.
|
|
The `runa-client` crate validates response protocol versions and request IDs.
|
|
Its `WebSocketClient` connects to
|
|
loopback endpoints using bounded WebSocket messages and correlation checks,
|
|
validating interleaved notifications while waiting for responses.
|
|
Connection establishment has a 5-second timeout; WebSocket writes and response
|
|
waits have 30-second timeouts. A failed network request invalidates that client connection.
|
|
No automatic retry is performed: loss of a response does not imply that the
|
|
operation failed to commit.
|
|
|
|
The interactive REPL uses WebSockets and never owns the image. Disconnecting clients
|
|
does not stop actors or reset source history; reconnecting clients can
|
|
retrieve them through `list_actors` and `list_sources`. The network executable
|
|
loads `source/actors/counter.rn` on image startup, not on connection.
|
|
The stdio transport and child-process client have been removed.
|
|
|
|
The TypeScript VS Code client issues `list_actors`, `inspect_actor`,
|
|
source file/version-control operations, `spawn`, `send`, `tick`, and `system`.
|
|
VS Code automatically displays writable image files under a `source` directory
|
|
in its dedicated Runa sidebar's source tree, without modifying workspace folders,
|
|
and caches read-only commit documents by connection, path, and revision.
|
|
It preserves i64 inputs and bounds the complete outgoing envelope as well as
|
|
incoming messages. It enforces loopback endpoints, bounded UTF-8 text messages,
|
|
version/request-ID/reply-kind checks, and portable snapshot value validation.
|
|
It decodes JSON integers as `bigint` and prints them losslessly, including
|
|
signed 64-bit state and unsigned 64-bit telemetry. It uses the same 5-second
|
|
connect and 30-second request timeouts, closes on transport/protocol corruption,
|
|
and never retries automatically. A well-formed server error is surfaced without
|
|
invalidating the stream. Server notifications trigger automatic refreshes of
|
|
the actor tree and open snapshot documents. Manual refresh remains available.
|
|
|
|
## WebSocket transport and notifications
|
|
|
|
### Actor editing and log subscriptions (v5)
|
|
|
|
- `edit_actor {actor_ref,state,messages}`: `state` is a tagged portable value or
|
|
null to leave it unchanged. `messages` is null to leave the mailbox unchanged,
|
|
or `{reserved,queued}` where reserved is a tagged value or null for no
|
|
reservation, and queued is an ordered array of tagged values. At least one
|
|
edit is required. Only paused/failed actors with no active execution
|
|
are editable; values and mailbox capacity are validated atomically.
|
|
- `set_log_level {actor_ref,level}`: level is one of `trace`, `debug`, `info`,
|
|
`warn`, `error`, `fatal`. Both mutations return `acknowledged` and invalidate
|
|
image views. Actor snapshots include `log_level`.
|
|
- `logs {actors,follow,after}`: actors is an array of references (empty means
|
|
all); after is an exclusive u64 sequence cursor. Reply kind `logs` carries
|
|
`{entries,next_sequence,has_more}`, with at most 128 entries per page.
|
|
Each entry is `{sequence,actor_ref,level,message}`. Missing actors are errors.
|
|
History queries are read-only and do not invalidate views.
|
|
- With follow true, the connection registers the filter before history is read.
|
|
Subsequent notification envelopes carry
|
|
`{event:"log",data:{sequence,actor_ref,level,message}}`. Reissuing `logs`
|
|
replaces the filter; follow false unsubscribes. Matching entries newer than
|
|
the reply's cursor are emitted after its reply, allowing history/follow
|
|
deduplication by sequence. Other connections receive no log events.
|
|
Transport and client buffers are bounded at 1,024 entries; lag is an explicit
|
|
connection error, not a guaranteed-delivery stream. Disconnect removes the
|
|
subscription. Image invalidations remain coalescible and independent of logs.
|
|
|
|
On `quit`, `exit`, Ctrl+C, or end-of-input, the CLI completes the WebSocket close
|
|
handshake with a 5-second deadline before releasing the connection. A failed
|
|
handshake is reported rather than treating an abrupt reset as a normal close.
|
|
|
|
Connect to `ws://127.0.0.1:3737/` (or a configured numeric loopback endpoint)
|
|
and offer the `runa-debug` WebSocket subprotocol. The server requires and echoes
|
|
that subprotocol. Plain TCP JSON Lines is no longer accepted by the network
|
|
listener. The request/response contract is version 5.
|
|
Each WebSocket text message contains exactly one JSON request, response, or
|
|
notification, without newline framing. Binary messages and malformed requests
|
|
terminate the connection. Fragmented text is supported with a 1 MiB aggregate
|
|
message limit; individual frames are also limited to 1 MiB. Compression is not
|
|
enabled. Upgrade handshakes have a 5-second timeout and writes have a
|
|
30-second timeout so stalled peers do not hold client slots forever.
|
|
|
|
The endpoint accepts only the root path and rejects requests carrying an
|
|
`Origin` header: browser-origin access is not enabled for this unauthenticated
|
|
local API. This is not authentication; local non-browser clients retain full
|
|
development privileges.
|
|
|
|
Every upgraded connection automatically receives non-destructive invalidations:
|
|
|
|
```json
|
|
{
|
|
"version": 5,
|
|
"event": {
|
|
"event": "image_changed",
|
|
"data": { "revision": 42 }
|
|
}
|
|
}
|
|
```
|
|
|
|
Successful mutating requests (spawn, send, tick, lifecycle controls,
|
|
source writes/commits/rollback, and system messages) increment a server-session revision
|
|
and notify all connected clients, including the initiator. Read-only requests,
|
|
failed mutations, and `get_events` do not generate invalidations. A tick
|
|
invalidates snapshots even when it only advances execution telemetry or causes
|
|
an actor failure. A successful send invalidates mailbox snapshots before any
|
|
handler runs. Notifications may appear between requests and their replies;
|
|
clients must distinguish them from correlated responses.
|
|
|
|
Revisions increase within one server session, not across server restarts.
|
|
Slow subscribers retain only the latest unsent revision; gaps are expected.
|
|
These notifications are hints to re-fetch authoritative data, not individual
|
|
runtime events, a durable log, an atomic snapshot revision, or replay.
|
|
On connection/reconnection, fetch a full listing first. The VS Code client
|
|
coalesces bursts and schedules another refresh if an invalidation arrives
|
|
during an in-flight read. Refresh never advances the scheduler or calls `get_events`;
|
|
the REPL's explicit `events` command still drains the separate runtime queue.
|
|
|
|
The current validation checks the protocol version, request ID, actor-reference
|
|
shape, structured-value limits, revision ID shape, non-empty paths, mailbox
|
|
capacity, and instruction budget. Canonical source paths, actor existence,
|
|
expected source identities, mailbox capacity at send time, and all runtime
|
|
invariants remain authoritative at the application-service boundary.
|