Journal entries

An Atom feed Atom feed of these posts is also available.

Questions and Answers

The Syndicated Actor Model (SAM) claims to enable conversations between software actors. Is this just a metaphor? Or is it anthropomorphic gloss to obscure details which wouldn’t hold up under examination? The question & answer (Q&A) protocol is a prime example of communication using what is arguably a literal application of human conversational concepts. By deconstructing Q&A we can examine the basic layering of SAM.

Protocol

In Q&A actors publish questions and observe corresponding answers. Q&A, like all Syndicate interactions, is expressed in the Preserves data language and uses a <q> record with a single field and an <a> record with two fields. The first field of either record is a request item and the second field of the answer record is usually a response wrapped in either an ok, or an error record. A canonical description of the protocol exists as a Preserves schema.

Using a factorial operation as an example, an actor might publish the question <q <factorial 12>> and observe an answer of <a <factorial 12> <ok 479001600>>. The question <q <factorial -1>> might yield an answer of <a <factorial -1> <error "not a natural number">>.

Q&A clearly fits in the publish and subscribe pattern (pub/sub), however the Syndicate protocol is not a pub/sub protocol. Pub/sub within SAM is provided by intermediary entities that implement the dataspace protocol on top of the Syndicate protocol. Actors that converse by Q&A do not necessarily implement the dataspace protocol, as will be explained later on.

Example

In pseudocode a consumer of factorials could be implemented as:

1
2
3
4
5
6
7
8
let n = 12

assert(<q <factorial $n>>)

onAssert(<a <factorial $n> <ok ?r>>) {
  print("factorial $n: $r")
  exit()
}

And a recursive factorial producer:

1
2
3
4
5
6
7
8
9
10
11
onAssert(<q <factorial ?x>>) {
  if x == 1 {
    assert(<a <factorial 1> <ok 1>>)
  } else if x > 1 {
    onAssert(<ok <factorial ($x -1)> <ok ?y>>) {
      assert(<a <factorial $x> <ok ($x * $y)>>)
    }
  } else {
    assert(<a <factorial $x> <error "invalid argument">>)
  }
}

Using unbounded linear recursion is usually a bad idea, but this shows that the <a> for a <q> can be arrived at by iterative Q&A.

Underlayer

By dissecting the Q&A protocol we can observe how the Syndicate protocol works and where some optimisations can be applied.

HTTP

In human terms, questions and answers are a request and response protocol. One of the most ubiquitous forms of request and response in interprocess communication would be HTTP which makes it a good reference point for comparison.

To summarise an HTTP GET interaction:

  • A user-agent opens a bi-directional stream to a web-server.
  • The user-agent sends into the stream a message containing the GET verb and an identifier for the resource to be gotten.
  • The server processes the request.
  • The web-server sends some framing information and a representation of the resource back to the other side of the stream.
  • One of the two parties closes the stream.

As HTTP APIs proliferate it has become increasingly difficult to distinguish cases where TCP provides a transport for HTTP and where HTTP provides framing for TCP.

Syndicate protocol

The Syndicate protocol differs in that instead of streams there are actors with inboxes. Messages can be addressed to actors and delivered to their inboxes but messages do not identify a sender. If a message is sent with an expectation of a response, then that message must contain a return reference for the actor receiving it.

Addressing and references within SAM are made using a capability model. SAM references address entities that are reachable via the mailbox of the actor that contains them. Syndicate refrences are stateful in that they are bound to the lifetime of the entities they refer to. References may also be attenuated to reject or alter certain communication patterns.

Sending a return reference within the body of every message isn’t necessary if the two parties make a stateful exchange of messages. The Syndicate protocol provides stateful messages as assertions. An assertion is a message containing content and a handle, and a retraction is a message containing a handle that invalidates a previously sent assertion. If there exists an active assertion with a reference to an actor entity, that entity is reachable. When all assertions of references to an entity are retracted, that entity becomes unreachable.

The assert-and-retract form of messages is so useful that the Syndicate protocol inverts the assertion as a special case, and defines a SAM message as a SAM assertion that is made with a retraction immediately following it.

A GET interaction could be reimagined using the Syndicate protocol like this:

  • A assertion arrives in the mailbox of a web-server: <GET «resource-id» «response-ref»>.
  • The server processes the request.
  • The webserver asserts a response to the reference bundled with the request: <ok «resource-representation»>.
  • The web-server elects to update a resource-representation with a retract followed by another assert to the peer.
  • The original GET request assertions is retracted.
  • The web-server retracts its assertions to the peer.

This only explains the server side of the interaction. The actor model avoids an omniscient view, even in a hypothetical scenario, and uses the perspective of actors to observe and describe the world. In the simple GET example it could be the same user agent-actor sending the message to the web-server and receiving the reply, but the web-server cannot make that assumption given the information that it has.

Dataspace protocol

The dataspace protocol is how the pub/sub pattern is realised within SAM and the Q&A protocol is layered upon the dataspace protocol. Dataspaces are entities that receive and retain arbitrary assertions and messages with an exception for an assertion of <Observe «pattern» «observer»>. An <Observe> assertion registers an reference to an observer with a pattern, and for any matching assertion made to the dataspace the tuple of captures made by the pattern is asserted to the observer.

A consumer of a factorial would assert the following to a dataspace:

  • The question: <q <factorial 12>>.
  • An observe pattern that captures within <a>: <Observe <group <rec a> {0: <group <rec factorial> {0: <lit 12>}> 1: <bind <_>>}> «consumer-ref»>.

A producer of factorials would assert:

  • An observe pattern that captures within <q>: <Observe <group <rec q> {0: <group <rec factorial> {0: <bind <_>>}>}> «producer-ref»>.
  • The answer to observed <q>: <a <factorial 12> <ok 479001600>>.

Datasubspace

Q&A protocol is good for high-level interaction between actors but is not without overhead. First, there is dataspace entity that must maintain a registry of observers and copies of the <q> and <a> assertions. Second, recursive Q&A uses a stack of assertion handlers that must be unwound on retraction of <q> assertions.

Actors that service queries without consulting others can be optimized to use bare Syndicate instead. In the case of factorials the producer could not register any observations and only respond to assertions of <factorial 𝑛 «result-ref»> with assertions of <factorial 𝑛 «result»> to «result-ref».

Rewrites

Entities that do not speak dataspace protocol can be registered as Q&A entities within a dataspace using capability attenuation. Attenuation is adding rules to Syndicate capabilties that are run against every assertion and message. Attenuation rules, or caveats, implement macaroon-style attenuation of authority but they are also useful as a minimal processing language that is common across all Syndicate implementations.

Following the factorial example, an observation could be made using the dataspace pattern:

1
<group <rec q> {0: <group <rec factorial> {0: <bind <_>>}>}>

which captures 𝑛 from <q <factorial 𝑛>>. A simple factorial entity as previously described would be registered as the observer but with the caveat:

1
<rewrite <arr [<bind SignedInteger>]> <rec factorial [<ref 0> «result-ref»]>>

on the entity capability which rewrites the assertion of [𝑛] from the pattern capture at the dataspace to an assertion of <factorial 𝑛 «result-ref»>. Within that caveat the «result-ref» is a capability of the dataspace attenuated with the caveat

1
<rewrite <rec factorial [<bind SignedInteger> <bind <_>>]> <rec a [<rec factorial [<ref 0>]> <ref 1>]>>

which rewrites <factorial 𝑛 𝑟> to <a <factorial 𝑛> 𝑟>.

In this way the logic within the factorial producer can be minimized and its Q&A interaction realised externally using only caveat rules.

Comparison of factorial implementations

Different techniques for implementing factorial can be visually analysed by rendering protocol traces. In each case there are separate actors for dataspace, consumer, and producers so that interactions are explicitly transactional. Where actor boundaries exist it is generally the case that interaction would be the same if each actor were isolated by process or hardware boundaries.

Recursive

Without looking at the details it is obvious that the recusive factorial is the wrong implementation. Close examination shows that determining the factorial of 5 requires 27 turns of SAM protocol before halting.

Loop

Determining an arbitrary factorial using a loop internal to an assertion handler only requires 9 turns of SAM protocol.

Loop with attenuation

Using a dataspace naïve factorial actor still runs 9 turns before halting but requires less handler state. This is visible as less “facet- stop” actions during teardown (this trace rendering tool should be improved to better track lifetimes of SAM facets and entities).

Areas for further research

Q&A chaining

Nested Q&A interactions could be chained together using rewrite caveats, this would be particularly useful within the scripting language of the Syndicate-server.

Promises

Q&A aligns neatly with Promise Theory.

Syndicate actors and the agents of Promises theory intersect on the principle of autonomy. To summarise:

  • Each actor or agent has a subjective view of reality. Actors may apply their own semantics to interactions with other actors.
  • Actors or agents cannot impose behavior on each other.
  • Collections of actors or agents can be collapsed into scale-free superagents.

To describe Q&A in the terms of Promises:

  • The assertion of <q 𝑟> is a publicly declared intent to observe an assertion of <a 𝑟 ?>.
  • An assertion of <a 𝑟 ?> is a promise that the information therein corresponds to the assertion of <q 𝑟>.
  • An assessment of whether a corresponding <a> has been published in response to a <q> can be made to determine if an agent is fulfilling it’s promise to respond to a class of questions.
  • Actors with public Q&A interfaces can hold private Q&A conversations with other actors to form a superagent.

Applying the formal model of Promise theory to SAM to analyze actor constellations is an open avenue of research, no doubt with additional layers of protocols for describing and assessing actor behavior.

Getting udev to describe your system as assertions

Emery Hemingway came up with an excellent hack for getting udev to automatically populate a dataspace with assertions describing the devices attached to a Linux system. As devices are plugged in, reconfigured, and unplugged, the system automatically creates, updates and removes files in /run/etc/syndicate/hardware describing each device.

The trick is to install a udev RUN rule to execute a script which uses udevadm info --json=short to write out a Preserves file. Preserves is, syntactically, a superset of JSON, so it’s possible to directly use JSON output from programs like this as Preserves.

A syndicate-server can then be configured to load all files in /run/etc/syndicate/hardware into a dataspace by placing something like

1
2
let ?devices = dataspace
<require-service <config-watcher "/run/etc/syndicate/hardware" { config: $devices }>>

in the server’s configuration.

udev configuration and script

The run rule, placed in a file named something like /usr/local/lib/udev/rules.d/50-syndicate.rules, should be something like this:

1
ACTION=="add|change|move|remove", RUN+="/usr/local/bin/udev-syndicate-rule.sh"

And the script that does the work of maintaining the Preserves files in response to trigger events from udev, placed in /usr/local/bin/udev-syndicate-rule.sh, should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh
# Script based on https://git.syndicate-lang.org/ehmry/syndev#assertions-from-udev

if [ -z "$DEVNAME" -o -z "$MAJOR" -o -z "$MINOR" ]
then
        exit
fi

HARDWARE_DIR=/run/etc/syndicate/hardware
PR_FILE=$HARDWARE_DIR/$MAJOR-$MINOR.pr

mkdir -p $HARDWARE_DIR

case "$ACTION" in
        remove)
                rm -f $PR_FILE
                ;;
        *)
                echo '<device ' > $PR_FILE
                udevadm info --json=short $DEVNAME >> $PR_FILE
                echo '>' >> $PR_FILE
                ;;
esac

After adding the rule and the script to your system, you will need to either reboot or run

1
udevadm control --reload-rules && udevadm trigger

to get things started.

Example output

After installing the script on my system, I see about 250 files appear in /run/etc/syndicate/hardware.

For example, if I pipe /run/etc/syndicate/hardware/13-78.pr through preserves-tool for pretty-printing, I see:

<device {
  "CURRENT_TAGS": ":power-switch:"
  "DEVNAME": "/dev/input/event14"
  "DEVPATH": "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/0000:02:00.0/0000:03:00.1/sound/card1/input14/event14"
  "ID_INPUT": "1"
  "ID_INPUT_SWITCH": "1"
  "ID_PATH": "pci-0000:03:00.1"
  "ID_PATH_TAG": "pci-0000_03_00_1"
  "ID_PROCESSING": "1"
  "LIBINPUT_DEVICE_GROUP": "0/0/0:ALSA"
  "MAJOR": "13"
  "MINOR": "78"
  "SUBSYSTEM": "input"
  "SYSNAME": "event14"
  "SYSNUM": "14"
  "TAGS": ":power-switch:"
  "USEC_INITIALIZED": "8598957"
}>

Configuring syndicate-server gatekeepers

A gatekeeper in Syndicate terminology is an object that “upgrades” long-lived cryptographic names into live references to some underlying object. There are analogous objects in CapTP. (In CapTP, gatekeepers might be built-in to the protocol rather than separate objects. I need to find out more.)

The syndicate-server program includes an implementation of the gatekeeper protocol. This post builds up a short example syndicate-server setup involving gatekeepers. (The following is based on this example config file from the syndicate-server git repository.)

The plan

We will create a TCP listener on port 9222, which speaks unencrypted protocol and allows interaction with the default/system gatekeeper, which has a single noise binding for introducing encrypted interaction with a second gatekeeper, which finally allows resolution of references to other objects.

The outer/default/system gatekeeper will not expose any functionality apart from upgrading to an encrypted, authenticated reference to the inner gatekeeper. All the real services we want to expose will be available via the inner gatekeeper.

The stanzas below are written in the syndicate-server scripting language. They can be placed in a .pr file, e.g. example.pr, and run with syndicate-server -c example.pr.

Creating an inner gatekeeper

First, build a space where we place bindings for the inner gatekeeper to expose.

let ?inner-bindings = dataspace

Next, start, secure, and publish the inner gatekeeper.

<require-service <gatekeeper $inner-bindings>>
? <service-object <gatekeeper $inner-bindings> ?inner-gatekeeper> [
  <bind <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs],
                 secretKey: #[qLkyuJw/K4yobr4XVKExbinDwEx9QTt9PfDWyx14/kg],
                 service: world }>
   $inner-gatekeeper #f>
]

The <bind ...> assertion exposes the $inner-gatekeeper object via the outer gatekeeper. Clients have to speak the noise protocol and know the public key #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs] to resolve a reference to the inner gatekeeper. The noise protocol itself sets things up to rule out man-in-the-middle and prove to the client it really is talking to the correct server.

You can use the syndicate-macaroon tool (e.g. syndicate-macaroon noise --service world --random) to generate fresh keys for use in noise bindings.

Exposing the outer gatekeeper over unencrypted TCP

Now, expose the outer gatekeeper to the world, via TCP. The system gatekeeper is a primordial syndicate-server object bound to $gatekeeper.

<require-service <relay-listener <tcp "0.0.0.0" 9222> $gatekeeper>>

Let’s imagine we are running on a host called syndicate.example; this hostname will be used in URLs later.

Binding a service to the inner gatekeeper

Finally, let’s expose some behaviour accessible via the inner gatekeeper.

We will create a service dataspace called $world, and we will require clients to hold a sturdyref to it. We will configure the inner gatekeeper to resolve the sturdyref to the $world dataspace.

let ?world = dataspace

We need to generate a strong name, a sturdyref, that we will bind to $world. For this, we can use the syndicate-macaroon tool.

Let’s choose the passphrase hello as our secret that no-one but the service itself should know.

Running syndicate-macaroon mint --oid a-service --phrase hello yields the sturdyref <ref {oid: a-service, sig: #[JTTGQeYCgohMXW/2S2XH8g]}>. That will act as a root capability for $world once we bind it at the inner gatekeeper, using the corresponding sturdy.SturdyDescriptionDetail:

$inner-bindings += <bind <ref {oid: a-service, key: #"hello"}> $world #f>

Configuration done!

That’s it! All together, here is the configuration we set up:

let ?inner-bindings = dataspace

<require-service <gatekeeper $inner-bindings>>
? <service-object <gatekeeper $inner-bindings> ?inner-gatekeeper> [
  <bind <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs],
                 secretKey: #[qLkyuJw/K4yobr4XVKExbinDwEx9QTt9PfDWyx14/kg],
                 service: world }>
   $inner-gatekeeper #f>
]

<require-service <relay-listener <tcp "0.0.0.0" 9222> $gatekeeper>>

let ?world = dataspace

$inner-bindings += <bind <ref {oid: a-service, key: #"hello"}> $world #f>

Accessing the $world from some remote client

Now, we can hand out paths to $world involving an initial noise step and a subsequent sturdyref/macaroon step. Clients using such paths benefit from an encrypted, authenticated channel (the noise layer), and use of the $world service is authorized by possession of a correctly-signed sturdyref.

The gatekeeper.Route that a client might use would be something like:

<route [<ws "ws://syndicate.example:9222/">]
  <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs], service: world }>
  <ref { oid: a-service, sig: #[CXn7+rAoO3Xr6Y6Laap3OA] }>>

There’s more that sturdyrefs can do: since they are based on Macaroons, they can be attenuated offline (!) to limit the kinds of things clients can assert at $world and the kinds of messages they can send to $world. The example config file gets into more detail.

Using Noise Protocol tunnels in Syndicate systems

The Syndicate ecosystem includes a few ways of gluing together dataspaces over a network.

To begin with, there was no authentication or authorization framework involved at all: programs just exchanged assertions and messages using an ancestor of the Syndicate protocol over a plain socket. There was no way of referring to entities/objects/actors at all: Syndicate at this point was data-only. (It turns out this is sufficient for a surprisingly large range of programs!)

Then, I figured out how to use ideas from Macaroons to introduce capabilities on the wire. This allows recovery of session-local object references from long-lived cryptographic names for use in a protocol session. Programs “upgrade” a name like

<ref {oid: a-service, sig: #[JTTGQeYCgohMXW/2S2XH8g]}>

to a real pointer they can use to send assertions and messages to.

Finally, I incorporated the Noise Protocol Framework, making it possible to use Syndicate to open encrypted, authenticated tunnels across untrusted links. The idea is to use a cleartext socket or websocket (or anything else!) to bootstrap communication, and then use assertions and messages related to Noise to open a Noise session across that foundation.

A small example

Here’s a small example from a real system I’m running.

I run a syndicate-server Docker container configured with, among other stanzas, the following service definition (these are fresh random keys, not the ones I’m using live):

let ?shared = dataspace
let ?sk = #[0LFU4CAWxrz0VVS9nY2uSc7EWJTusZUcOP0HUpkwSkE]
let ?pk = #[Y3NCy2ZPbmaH62fFI0sl0YOZmM7K7PhVTmRFmgCAgVA]
@<connect-using # <-- this is just a comment, really
  <route [<ws "ws://LOCATION.TO.MY.SERVER.WEBSOCKET/">]
   <noise {service: a-service key: #[Y3NCy2ZPbmaH62fFI0sl0YOZmM7K7PhVTmRFmgCAgVA]}>>
 >
<bind <noise { service: a-service, key: $pk, secretKey: $sk }> $shared #f>

Then I can securely access the $shared dataspace from a remote syndicate-server instance using a stanza like this:

? <resolve-path
   <route [<ws "ws://LOCATION.TO.MY.SERVER.WEBSOCKET/">]
    <noise {service: a-service key: #[Y3NCy2ZPbmaH62fFI0sl0YOZmM7K7PhVTmRFmgCAgVA]}>>
   _
   _
   <accepted ?sharedDs>>
<the-shared-dataspace-is $sharedDs>

A lightly tricky piece of the puzzle is that the resolve-path handler isn’t built-in to syndicate-server yet: for that part, I’m using a tiny node.js service until I have a chance to implement the resolution logic in Rust. Watch this space.

Multiaddr

In the #spritely IRC channel, mala mentioned the multiaddr specification.

From the site, “Multiaddr is a format for encoding addresses from various well-established network protocols.” The aim is to avoid “[leaving] much to interpretation and side-band context”, and to allow people to “build applications that will work with network protocols of the future, and do not accidentally ossify the stack.”

Syndicate defines something similar (though rudimentary) in its transportAddress module and in its notion of a Route.

Semantics for Multiaddr

Multiaddr informally defines its semantics, and gives both a human-readable and a machine-readable concrete syntax.

Let’s borrow preserves and propose a better specification for multiaddr semantics:

  • A multiaddr value (a Multiaddr) is a Sequence of protocol addresses. The sequence is implicitly understood to describe a layered protocol stack, with leftward addresses (the “bottom” of the stack) acting as substrate for rightward addresses.

  • A protocol address (an Address) is a record with its label being a Symbol representing a protocol name (a ProtocolName), and its fields being zero or more protocol-specific Values.

  • Each kind of Address must define a canonical form. Use of the canonical form is mandatory. For example, an ip6 address could define a rule that the associated string must always conform to IETF IPv6 canonical format.

For example, the multiaddr examples written

1
2
3
4
5
/ip4/127.0.0.1/udp/9090/quic
/ip6/::1/tcp/3217
/ip4/127.0.0.1/tcp/80/http/baz.jpg
/dns4/foo.com/tcp/80/http/bar/baz.jpg
/dns6/foo.com/tcp/443/https

could denote the Values

1
2
3
4
5
[<ip4 127 0 0 1> <udp 9090> <quic>]
[<ip6 "::1"> <tcp 3217>]
[<ip4 127 0 0 1> <tcp 80> <http "/baz.jpg">]
[<dns4 "foo.com"> <tcp 80> <http "/bar/baz.jpg">]
[<dns6 "foo.com"> <tcp 443> <https "/">]

Schema for Multiaddr

Much of this structure can be captured by a preserves schema definition. Given such a schema, the multiaddr text and binary syntaxes become special-purpose syntax for preserves Values conforming to the schema.

version 1 .

Multiaddr = [Address ...] .
Address = <<rec> @protocolName symbol @detail [any ...]> .

WellKnownProtocol =
/ <ip4 @a int @b int @c int @d int>
/ <ip6 @addr string>
/ <dns4 @dnsName string>
/ <dns6 @dnsName string>
/ <udp @port int>
/ <tcp @port int>
/ <quic>
/ <http @path string>
/ <https @path string>
/ @unknown Address
.

Side-conditions such as bounding the integers in an ip4 address to the range [0..255] are not currently expressible in preserves schema.

Also, these definitions are too simple, particularly in the case of HTTP(S), where much more of the structure of an HTTP url (username, password, path, query, fragment, etc.) can and should be parsed out.

In-browser JavaScript+Syndicate, round two

Since last time, I’ve managed to shrink the boilerplate for experimenting with Syndicate in the browser a little further.

Save the following in an HTML file, and open it:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<script src="https://cdn.jsdelivr.net/npm/@syndicate-lang/browser-stdenv"></script>
<script type="syndicate">
  spawn named 'clicker' {
    field counter = 0;
    const b = new SyndicateHtml2.Widget(t => t`<button>clicks: ${counter.value}</button>`)
          .setParent('body')
          .on('click', () => counter.value++);
  }
</script>

You should be greeted with a click-counter button.

Here’s the same as a codepen:

See the Pen Untitled by Tony Garnock-Jones (@leastfixedpoint) on CodePen.

In-browser compiler for JavaScript+Syndicate to plain JavaScript

To make experimenting with Syndicate/JS a bit easier, I’ve just dusted off an old idea: getting the compiler from JavaScript with Syndicate DSL extensions down to plain JavaScript to run in the browser.

It’s (relatively) simple. Load a few modules, perhaps via a CDN:

1
2
3
4
5
<script src="https://cdn.jsdelivr.net/npm/@preserves/core/dist/preserves.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@syndicate-lang/core/dist/syndicate.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@syndicate-lang/compiler/dist/syndicate-compiler.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@syndicate-lang/html/dist/syndicate-html.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@syndicate-lang/compiler/dist/syndicate-browser-compiler.js"></script>

Then, add <script type="syndicate"> tags with Syndicate/JS code in them:

1
2
3
4
5
<script type="syndicate">
  spawn {
    assert "Hello, world!";
  }
</script>

The syndicate-browser-compiler.js script automatically translates such scripts into plain JavaScript:

1
2
3
4
5
6
<script>
  const __SYNDICATE__ = Syndicate;
  __SYNDICATE__.Turn.active._spawn(() => {
    __SYNDICATE__.Turn.active.assertDataflow(() => ({ target: currentSyndicateTarget, assertion: "Hello, world!" }));
  });
</script>

Here’s a small (live!) example, hosted at codepen:

See the Pen Standalone Syndicate/JS page, December 2023 by Tony Garnock-Jones (@leastfixedpoint) on CodePen.

It’s still too hard to get started with Syndicate, but this is at least a step in the right direction.


(Just in case the codepen goes away, here is the same example as a standalone webpage.)

syndicate-pty-driver: Interactive subprocesses over Syndicate

I’ve just released syndicate-pty-driver, a small Rust program that responds to a Syndicate protocol for allocating PTYs and spawning subprocesses attached to them.

This allows programs collaborating using Syndicate to run interactive subprocesses on the machine that’s running the PTY driver.

As an example, I’m playing around with a “VR”-like environment for use in a browser on a cellphone with a Cardboard headset and a bluetooth keyboard, loosely based on an idea by John Dean and Joan Creus-Costa (“BonelessVR: A lightweight, high-productivity desktop environment for Google Cardboard”):

Screenshot of my little VR project running on my phone

The phone runs Syndicate/js in the browser, speaks the Syndicate protocol across a websocket, and a syndicate-server instance at the other end supervises a syndicate-pty-driver. The whole thing is secured by Syndicate’s object capabilities and its use of the Noise protocol for secure transport.

TypeScript IDE Syndicate plugin: good news and bad news

Because the TypeScript implementation has been moving on along its own roadmap, the IDE-support (Language Server Protocol, LSP) part of the TypeScript compiler plugin that I wrote (npm, git) has stopped working properly with recent TypeScript releases.

ts-plugin versions older than 0.15.2 don’t work well with TypeScript 4.9; it’s possible they haven’t worked well since TypeScript 4.5.

The good news

The good news is that @syndicate-lang/ts-plugin version 0.15.2 includes a few changes that make it work much better with TypeScript 4.9. So if you update to 0.15.2, you should see good support for the Syndicate DSL overlaid atop the normal TypeScript support in Emacs, VSCode, etc.

The bad news

The bad news is that the monkeypatching I’ve been doing to hook into the necessary parts of the TypeScript tsserver will no longer work for TypeScript versions 5.0 and higher. The problem is that, in tsserver 4.9, simple properties were used for values like ts.createSourceFile and ts.server.ScriptInfo, so the ts-plugin was able to hook them to insert the Syndicate DSL expansion process. In 5.0, by contrast, Object.defineProperty is used to define a non-writable, non-configurable property. It’s essentially game over for the existing ts-plugin approach at this point.

So what now?

Having ts-plugin IDE support is really nice for Syndicate programming. So I feel compelled to find some kind of solution.

Perhaps I need to write a real LSP implementation, backing into a captive tsserver instance?

I’ve opened an issue on the ts-plugin repository about this.

Questions from IRC

A few days ago, on the #syndicate IRC channel, user observeSynd (maybe observeSyndicate?) asked a few good questions:

  1. Can I take ‘endpoints’ as discussed in the dissertation to be the same thing as ‘entities’ as outlined in the Synit SAM (chapter 18) theory section?
  2. Are there any examples in any of the implemetations showcasing how (nested) facets work?
  3. Have any of the implementations documented their DSL as would be found at https://syndicate-lang.org/doc/syndicate/ ?

Here are my (short) answers:

  1. Endpoints aren’t quite entities, but they’re related. Entities are a later development. They’re much closer to traditional actor references or, even closer still, to E-style object references. Endpoints are a kind of intermediate concept: they’re a combination of a pattern, a subscription, and an object reference. I think the entity-based approach is cleaner, and in hindsight, they were not well described in my dissertation. Perhaps this was a clue that there was something simpler lurking underneath, waiting to be discovered and described.

  2. Nested facets: yes, throughout the available examples. However, nesting of facets isn’t lexically apparent, so you have to read the program while imagining the dynamics.

  3. Sadly, there’s no documentation of the DSLs yet. I’m embarrassed about this. Hopefully I’ll find time to rectify the situation soon! I’m afraid the various example programs are the only references out there right now.