Rigs
A Slipway Rig has two main purposes:
- It composes together Components in a way that generates a useful output.
- It assigns permissions to Components, giving them controlled escape hatches from their sandbox.
It does both of these things through a simple JSON configuration file, which is named after the Rig (e.g. my_rig.json
).
At the top level, the Rig file looks like this:
{
"description": "...",
"constants": {
// ...
},
"rigging": {
// ...
}
}
We'll go through each of these properties.
description
Optional. The description is just a simple textual description of what the Rig does. It is only there to help users who are looking at the Rig.
constants
Optional. This can contain any arbitrary data structure. It is a convenient place to put static data which will be referenced by multiple Components.
rigging
This is the main part of the Rig, where components are composed. It is a map from Component Handle to the rigging for that component.
{
"description": "...",
"constants": {
// ...
},
"rigging": {
"component_one": {
// ...
},
"component_two": {
// ...
},
// ...
}
}
The Component Handles can be anything you like, but must consist of only lowercase alphanumeric characters and underscores. The handles are used to refer to each component within the rigging.
The Component Rigging for a given Component Handle is structured as follows:
"component_one": {
"component": "...",
"input": {
// ...
},
"allow": [
// ...
],
"deny": [
// ...
],
"callouts": {
// ...
}
}
rigging.*.component
This is the Component Reference, which uniquely identifies which component should be loaded and executed.
rigging.*.input
The input is where you specify what data should be passed into the Component.
It is arbitrary JSON but with one important feature: At any point in the structure you can specify specially formatted JSONPath queries which will be substituted for actual data at runtime.
In the folowing example component_two
:
- Requests data from the output of
component_one
using the query$.rigging.component_one.output
. - Requests the value of the constant
$.constants.foo
.
{
"description": "...",
"constants": {
"foo": 123
},
"rigging": {
"component_one": {
"component": "...",
"input": {
"value": 1
}
},
"component_two": {
"component": "...",
"input": {
"foo": "$.constants.foo",
"bar": "$.rigging.component_one.output",
}
},
// ...
}
}
The queries are standard JSONPath syntax, with some minor changes for convenience:
- Queries starting with
$.
will return exactly one value, and will error if either no values or multiple values are found. - Queries starting with
$?
will return zero or one values, and error if multiple values are found. If no values are found the property is omitted. - Queries starting with
$*
will return zero or more values, as an array.
Queries for a Component's output, of the form $.rigging.<component_handle>.output
, are so common that we provide a $$
shortcut syntax of $$.<component_handle>
.
So the following are equivalent:
$.rigging.component_one.output
$$.component_one
As are these:
$?rigging.component_one.output.foo[0].bar
$$?component_one.foo[0].bar
Component Execution Order
Slipway uses the inputs of each Component to determine the required execution order so that it can fully resolve each Component's input just before it executes.
If any cycles are detected between components, the Rig will fail with an error before it runs.
In the above example, Slipway will determine that it must execute component_one
before component_two
, so that the JSONPath
query in component_two
can be resolved.
rigging.*.allow
and rigging.*.deny
By default Components are sandboxed can only operate on their inputs to generate their outputs. They cannot interact with the external environment, such as making HTTP requests, or accessing files, fonts or environment variables.
This allows you to run Components written by other people, confident they aren't accessing your files or calling home when they execute.
However the Rig can give Components permissions to do these things using the allow
field, and revoke granted permissions using the deny
field.
For example, to allow a component to access to the AWS_DEFAULT_REGION
environment variable, you would specify:
{
"allow": [
{ "permission": "env", "exact": "AWS_DEFAULT_REGION" }
]
}
To allow a component to access to all environment variables starting with AWS_
, you would specify:
{
"allow": [
{ "permission": "env", "prefix": "AWS_" }
]
}
To allow a component to access to everything except local files and environment variables:
{
// ...
"allow": [
{ "permission": "all" }
],
"deny": [
{ "permission": "files" },
{ "permission": "env" }
]
}
The all
permissions should be used with caution, and probably only on Components you've authored yourself.
For complete documentation on permissions, see the Permissions page.
Trusting Rigs
The permissions specified in Rigs allow you to trust Components, by granting them access only to the resources they need to do their job. But what about Rigs? If you've downloaded a Rig from the internet, how do you know it's not giving components too many permissions?
Obviously you could inspect the Rig yourself, and indeed this is good practice. However, Rigs themselves are also sandboxed by default.
Essentially a Rig can only pass on permissions the Rig itself has been given. If the Rig hasn't been given permissions to access environment variables, then no components executed within that Rig are able to access environment variables either.
You give Rigs permissions either on the command line when running slipway run
,
or in the slipway_serve.json
configuration file when hosting Rigs.
For complete documentation on permissions, see the Permissions page.
rigging.*.callouts
Some Components will, during their execution, call other Components. All Component references that could be called by a Component must be declared in advance so that Slipway can resolve all references before execution starts.
This is often done in the Component's configuration file. However sometimes the Component itself won't know what other Components it will be required to call, in which case they can be specified in the Rigging instead.
The callout for a given Component Handle is structured as follows:
"callout_handle_one": {
"component": "...",
"allow": [
// ...
],
"deny": [
// ...
],
}
The component
, allow
and deny
fields are the same as those defined for a component in the rigging above.
See the component, allow and deny sections for more information.
The slipwayhq.render
component can call other rendering components (such as chart renderers) during execution.
It passes them the exact width and height they should render to perfectly embed the result into the final output canvas.
However it only finds out which Components it needs to call at runtime when analyzing its input.
It therefore cannot declare all possible Component references within the render
Component's configuration file.
Instead, if you wanted the slipwayhq.render
Component to embed a chart rendered using the slipwayhq.echarts
Component
in the output, you would need to declare something like the following in the Component rigging:
{
// ...
"callouts": {
"echarts": {
"component": "slipwayhq.echarts.0.5.1",
"allow": [
{ "permission": "fonts" },
{ "permission": "registry_components" }
]
}
}
}
See the Render Component section for more details.
An Example Rig
What does this all look like in practice? Here is a real Rig I've been running to display my house solar and battery data on a TRMNL eInk screen:
{
"description": "Renders GivEnergy data with ECharts",
"rigging": {
"ge": {
"component": "jamesthurley.givenergy_cloud.0.5.0",
"allow": [
{ "permission": "http", "prefix":"https://api.givenergy.cloud/" },
{ "permission": "env", "prefix":"GIVENERGY_" }
],
"input": {}
},
"render": {
"component": "slipwayhq.echarts.0.5.1",
"allow": [
{ "permission": "fonts" },
{ "permission": "registry_components", "publisher": "slipwayhq", "name": "svg" },
{ "permission": "registry_components", "publisher": "slipwayhq", "name": "echarts_svg" }
],
"input": {
"width": 800,
"height": 480,
"chart": "$$.ge.chart",
"theme": {
"backgroundColor": "#FFF"
}
}
}
}
}
In this case my custom jamesthurley.givenergy_cloud
Component fetches solar and battery data and
outputs an ECharts chart definition, which is then rendered by the slipwayhq.echarts
Component.