Management REST API
The management API lets you inspect a running libsig project, start, stop and
restart its services, browse the available capsules, and create, edit and
delete capsule configurations and services over HTTP. It is optional: it exists only when libsig
is built with -DSIG_NET=ON -DSIG_HTTP_SERVER=ON -DSIG_MGMT=ON and the host
registers the three runtime modules (sighost -m <port> does this for you).
Enabling the API
Section titled “Enabling the API”With sighost:
cmake -S . -B build -DSIG_NET=ON -DSIG_HTTP_SERVER=ON -DSIG_MGMT=ONcmake --build buildbuild/host/posix/sighost -p examples/basic_counter -m 8080-m <port> serves the API on 127.0.0.1:<port>; -m <addr>:<port> binds a
specific numeric IPv4 address (e.g. -m 192.168.64.2:8080, or 0.0.0.0:8080
for all interfaces; see Security).
-o <origin> enables CORS for that origin (-o '*' for any), so a browser UI
hosted elsewhere can call the API; for timeouts, register the modules yourself
(see below).
The browser UI in ui/ uses this API. Live signal values are
streamed by a separate WebSocket endpoint on the same port, described in
Live signal feed.
REST is one transport over the management API: the
operations, their results, errors and locking live in sig_mgmt, and
sig_mgmt_http maps routes, JSON and HTTP statuses onto them. Routes, statuses
and bodies are unchanged by that split, with these differences to know about:
- Every response object’s key order is unspecified (bodies are rendered
from a tree, not built in a fixed order), and configuration documents in
responses (
config) are serialized compactly, so their whitespace is unspecified too. Clients must not depend on either. restartcan answer409 already_runningwhen it loses a race with a concurrentstartof the same service.- Running out of start retries (the service kept being edited or stopped
between rebuild and start), or the service vanishing after the action, is
answered with the
503“runtime is not running” body. That body is shared by those causes and by the runtime really not running. - Numbers in
configdocuments are re-rendered from a parsed tree: a double keeps a.or exponent (5.0stays a double) but its spelling can change (1e3comes back as1000.0); an integer outside the int64 range comes back as a double and loses precision; and when a document repeats an object key, only one value survives.
From your own host program, register http_server, then mgmt, then
mgmt_http (the glue looks the other two up when it is created). The API is
served at the root path (/system, /services/...):
sig_runtime_register_module(rt, sig_http_server_module(), "port=8080 cors_origin=http://localhost:3000");sig_runtime_register_module(rt, sig_mgmt_module(), NULL);sig_runtime_register_module(rt, sig_mgmt_http_module(), NULL);sig_mgmt_http_module() takes no config: a non-empty one is rejected with
SIG_ERR_INVALID_ARG, so old port=... use fails loudly. The HTTP server
listens only if some module registered routes.
Transport configuration
Section titled “Transport configuration”The transport settings belong to the http_server module. Its config string is
space-separated key=value pairs. Unknown
keys, tokens without =, and out-of-range values make sig_runtime_start
fail with SIG_ERR_INVALID_ARG.
| Key | Default | Meaning |
|---|---|---|
bind |
127.0.0.1 |
Numeric IPv4 address to listen on. Anything not starting 127. logs a warning (see Security). |
port |
8080 |
0–65535. 0 picks an ephemeral port; read it back with sig_http_server_get_port(). |
cors_origin |
none | Sent verbatim as Access-Control-Allow-Origin; enables OPTIONS preflight. Must not contain CR/LF. |
timeout_ms |
5000 |
1–600000. Total time allowed to receive a complete request, and the per-write timeout. |
Conventions
Section titled “Conventions”- Format. Every response, including errors, is
application/json. - Methods.
GETreads state;POSTperforms an action;PUT,PATCHandDELETEedit the project (see Editing). Methods are case-sensitive.PUTandPATCHbodies are JSON (Content-Type: application/json, aContent-Lengthheader, up to 8192 bytes); other endpoints ignore a body. - Paths. Segments may be percent-encoded. Splitting on
/happens before decoding, so%2Finside a name cannot create a new path segment. A query string or fragment on the request target is discarded. - Ordering. Lists are sorted by name, except a capsule’s terminal lists
(declaration order) and
links(seeGET /services/{name}); object keys appear in a fixed order. - Connections. One request per connection; every response carries
Connection: close. - Names. A service name is its
services/<name>.cfgfile stem. An instance name is the name of the capsule configuration it was created from (configurations/<name>.cfg), so it is unique within a service.
Endpoints
Section titled “Endpoints”| Method | Path | Success | Purpose |
|---|---|---|---|
GET |
/system |
200 | Version, uptime, registered runtime modules |
GET |
/services |
200 | All services with state and instance count |
GET |
/services/{name} |
200 | One service and its instances |
GET |
/services/{name}/instances/{instance} |
200 | One instance with configured properties |
POST |
/services/{name}/start |
200 | Start a service |
POST |
/services/{name}/stop |
200 | Stop a service |
POST |
/services/{name}/restart |
200 | Stop then start a service |
POST |
/system/shutdown |
202 | Ask the host process to shut down |
GET |
/capsules |
200 | The compiled-in capsules (the palette) with their terminals |
GET |
/configurations |
200 | All capsule configurations |
GET |
/configurations/{name} |
200 | One configuration and the services that use it |
PUT |
/configurations/{name} |
201 / 200 | Create or replace a configuration |
PATCH |
/configurations/{name} |
200 | Merge a patch into a configuration |
DELETE |
/configurations/{name} |
200 | Delete a configuration no service uses |
POST |
/configurations/{name}/rename |
200 | Rename a configuration, and the references to it |
GET |
/services/{name}/config |
200 | A service’s on-disk definition |
PUT |
/services/{name} |
201 / 200 | Create or replace a service |
PATCH |
/services/{name} |
200 | Merge a patch into a service |
DELETE |
/services/{name} |
200 | Stop and delete a service |
GET /system
Section titled “GET /system”{ "version": "0.0.1", "uptime_seconds": 12.504, "modules": ["http_server", "mgmt", "mgmt_http"]}modules lists registered runtime modules in registration order.
uptime_seconds is a number of seconds since the runtime last started.
GET /services
Section titled “GET /services”{ "services": [ { "name": "test", "state": "running", "dirty": false, "instances": 3 } ]}state is "running" or "stopped". instances is the number of capsule
instances the service owns. dirty is true when the service’s definition, or a
capsule configuration it uses, was edited after the running service was built;
a restart picks the change up (see Editing).
Returns 503 unavailable if the runtime has no loaded project (i.e. it is not
running).
GET /services/{name}
Section titled “GET /services/{name}”{ "name": "test", "state": "running", "dirty": false, "instances": [ { "name": "Count", "configuration": "Count", "type": "counter", "inputs": ["__default_terminal_value"], "outputs": [] }, { "name": "Sim", "configuration": "Sim", "type": "simulator", "inputs": [], "outputs": ["__default_terminal_value"] }, { "name": "SimToo", "configuration": "SimToo", "type": "simulator", "inputs": [], "outputs": ["__default_terminal_value"] } ], "links": [ { "from": { "instance": "Sim", "terminal": "__default_terminal_value" }, "to": { "instance": "Count", "terminal": "__default_terminal_value" } }, { "from": { "instance": "SimToo", "terminal": "__default_terminal_value" }, "to": { "instance": "Count", "terminal": "__default_terminal_value" } } ]}type is the name of the capsule (the "type" key in the configuration’s .cfg); configuration is the configuration the instance was created from.
inputs and outputs are the terminal names the capsule declares, in
declaration order (not name-sorted). links is the
wiring from the service file’s receivers, sorted by source instance, source
terminal, destination instance, destination terminal. A link can name a
terminal the capsule does not declare, because config files are not checked
against the capsule’s terminals; clients should show such links rather than
drop them. If a service file lists the same receiver twice, the link
appears twice, matching how signals are delivered. All three arrays are always present and empty when there is nothing
to list. 404 not_found if the service does not exist.
GET /services/{name}/instances/{instance}
Section titled “GET /services/{name}/instances/{instance}”{ "name": "Sim", "service": "test", "configuration": "Sim", "type": "simulator", "properties": { "end": 9, "interval_ms": 250, "start": 0, "step": 1 }}properties are the values from the capsule configuration’s attr_value object
(sorted by key). They are the configured values, not live state: runtime
state such as the counter’s current count is not exposed. Property values are
reported as JSON ints, booleans, doubles or strings; any other kind of value
would be reported as null (config files cannot currently produce them).
404 not_found if either the service or the instance does not exist.
POST /services/{name}/start, /stop, /restart
Section titled “POST /services/{name}/start, /stop, /restart”All three return the service’s resulting state:
{ "name": "test", "state": "running" }The call blocks until the service’s capsule start/stop hooks have run, so the reported state is settled by the time you receive it.
| Endpoint | Notes |
|---|---|
start |
409 already_running if the service is already running. 422 invalid_config if a capsule’s configuration cannot start (for example a missing required property in a hand-written file); the service stays stopped and the host log names the property. |
stop |
Idempotent: stopping a stopped service returns 200 with "state": "stopped". |
restart |
Stop then start, performed atomically with respect to other service-control requests. |
All three return 404 not_found for an unknown service and 503 unavailable
if the runtime is not running. A service whose config has "auto_start": true
is started at boot; others stay stopped until you POST .../start.
GET /capsules
Section titled “GET /capsules”The palette: the compiled-in capsules a project can use, sorted by name, with the terminals each declares (in declaration order) and its property table.
{ "capsules": [ { "name": "counter", "category": "state", "inputs": ["__default_terminal_value"], "outputs": ["__default_terminal_value"], "properties": [ { "name": "report_interval_ms", "type": "int", "required": false, "default": 1000, "min": 1, "description": "Milliseconds between reports" } ] }, { "name": "simulator", "category": "source", "inputs": [], "outputs": ["__default_terminal_value"], "properties": [ ... ] } ]}category is source, sink, flow or state; it is omitted for a capsule
that does not declare one.
Each property has name, type (int, bool, double or string),
required and description, in declaration order. A property that is not
required has a default of its type; ints may have min and max; strings may
have choices (matched case-insensitively). A capsule with no property table has
"properties": [] and accepts any properties.
A capsule is a compiled-in implementation. A capsule configuration
(configurations/<Name>.cfg) is a named configuration of one capsule; an
instance is one configuration used by one service. See the Naming section of
architecture.md. This is not the runtime-module list in
GET /system.
GET /configurations
Section titled “GET /configurations”{ "configurations": [ { "name": "Count", "type": "counter" }, { "name": "Sim", "type": "simulator" } ]}Sorted by name. type is the capsule the configuration configures.
GET /configurations/{name}
Section titled “GET /configurations/{name}”{ "name": "Sim", "type": "simulator", "config": { "type": "simulator", "attr_value": { "end": 9, "interval_ms": 250 } }, "used_by": ["test"]}config is the configuration’s stored document. used_by lists the services
whose definition has an instance of it, sorted. 404 not_found if absent.
GET /services/{name}/config
Section titled “GET /services/{name}/config”{ "name": "test", "config": { "auto_start": true, "execution": [] }, "dirty": false }The service’s stored definition, which is what editing changes. It can differ
from GET /services/{name}, which shows the live service, until the service is
restarted.
Editing
Section titled “Editing”The write endpoints change the project’s .cfg files; the running system
picks a change up when the affected service next starts. There is no hot-apply.
-
Names must match
[A-Za-z0-9_-]{1,64}(they become file names); anything else is400 bad_request. -
PUTcreates (201) or replaces (200) the whole document, using the formats in configuration.md.PATCHis a JSON Merge Patch (RFC 7386): objects merge, scalars and arrays replace.nullis rejected with422, so remove a key byPUTting the whole document. -
Validation is the same as loading a project, plus the capsule’s property table (see
GET /capsules) for a configuration: bad JSON, a wrong shape, an unregistered capsule type, a missing required property, a value of the wrong type or out of range, an undeclared property, anexecutionentry naming a missing configuration, or a receiver naming a missing instance is422 invalid_config; themessagenames the offending property when there is one ("rate: required","step: must be >= 1","enb: unknown property"), and the host log has the rest. Nothing is written on failure, and a failed disk write leaves the previous file and definition in place. -
Persistence. Edits are saved through the host’s persistence store; the file backend writes
<name>.cfg.tmpand renames it, in compact JSON. -
Editing a running service does not disturb it. Its definition changes, the service reports
"dirty": true, and the change takes effect onstopthenstartorrestart. A stopped service is rebuilt immediately. Editing a configuration marks every service that uses it dirty. -
DELETEof a service stops it first, then removes its definition and file.DELETEof a configuration that any service definition still uses is409 configuration_in_use. -
Renaming a configuration (
POST /configurations/{name}/renamewith{"name": "NewName"}) renames its file and rewrites theexecutionentries and receivers that name it in every service definition that uses it, so the wiring survives. The response is the configuration under its new name. A running service is not disturbed: it keeps its instances under the old name, its stored definition now uses the new one, and it reports"dirty": trueuntil it is restarted. Stopped services are rebuilt immediately. The service files are rewritten as compact JSON, so their formatting and key order change. An unknown{name}is404, an invalid new name or body is400, and a new name that already belongs to a configuration is409 name_taken. The configuration is validated as it would be by aPUT. If a file cannot be written, the rename is undone and nothing changes. Only if the files still cannot be written while undoing can it stop part way. Both names then exist, each service uses one of them, and what is running matches what is on disk.
Write responses carry the resulting document and, for services, dirty:
{ "name": "extra", "config": { "execution": [ { "name": "Log2" } ] }, "dirty": false }DELETE returns { "name": "extra", "deleted": true }.
POST /system/shutdown
Section titled “POST /system/shutdown”{ "status": "shutting_down" }Returns 202 Accepted immediately and records a shutdown request; the host
then stops all services and exits. The response is sent before the process
exits, but there is no further confirmation.
Errors
Section titled “Errors”Errors produced by the management module have this shape:
{ "error": "not_found", "message": "no such service" }Errors produced by the HTTP transport before a request reaches the management
module have only the error field:
{ "error": "request_timeout" }| HTTP | error |
When |
|---|---|---|
| 400 | bad_request |
Malformed request line/headers, or a malformed path (bad %XX escape, encoded NUL) |
| 404 | not_found |
Unknown path, unknown service or instance, or more path segments than any route has |
| 405 | method_not_allowed |
The path exists but not for this method (also OPTIONS when CORS is not configured) |
| 408 | request_timeout |
The full request did not arrive within timeout_ms |
| 409 | already_running |
start on a running service |
| 409 | configuration_in_use |
DELETE of a configuration a service definition uses |
| 409 | name_taken |
A rename onto the name of an existing configuration |
| 411 | length_required |
PUT/PATCH without Content-Length (transport) |
| 415 | unsupported_media_type |
PUT/PATCH with a non-JSON Content-Type (transport) |
| 422 | invalid_config |
A written document fails validation, or start of a service whose capsule configuration is invalid |
| 413 | payload_too_large |
Content-Length over 8192 |
| 431 | request_too_large |
Request head over 8192 bytes, or target over 512 |
| 500 | internal |
Allocation failure or unexpected internal error |
| 503 | unavailable |
The runtime is not in the RUNNING state |
401 unauthorized exists in the transport but is currently unreachable: there
is no authentication yet (see Security).
Transport behaviour and limits
Section titled “Transport behaviour and limits”- HTTP/1.1 only, IPv4 only, no TLS, no keep-alive, no chunked request bodies.
- A single server thread handles one request at a time. A slow client delays
others until
timeout_msexpires; a longstart/stopblocks the server while the service’s hooks run. - Limits: head 8192 bytes, request target 512 bytes, body 8192 bytes.
- CORS. With
cors_originset, every response carriesAccess-Control-Allow-Origin, andOPTIONSon any path returns204withAccess-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS,Access-Control-Allow-Headers: Content-TypeandAccess-Control-Max-Age: 600. Without it,OPTIONSis405. - WebSocket upgrade hand-off. A module can register an upgrade prefix
(
sig_http_server_register_upgrade). AGETwithUpgrade: websocketandConnection: Upgrade(names and tokens case-insensitive;Connectionmay be a list) on such a path is validated and the connection is handed to that module, which writes the101itself; the HTTP server writes nothing and no longer tracks the connection (in the event-loop transport it stops counting toward the connection cap). Validation:Sec-WebSocket-Versionmust be13andSec-WebSocket-Keynon-empty, else400 bad_request; anOriginheader, if present, must equalcors_origin(orcors_originis*), else403 forbidden_origin(so with nocors_originany browser origin is refused; a client that sends noOriginis allowed). If the module cannot take the connection the reply is503 unavailable. An upgrade request on a path with no upgrade prefix is routed like any otherGET. The upgrade is as unauthenticated as the rest of the API (see Security): theOrigincheck only stops other web pages in a browser, not other clients. - On macOS/BSD,
SO_REUSEADDRallows a bind of127.0.0.1:Peven if another process is listening on0.0.0.0:P, so “address in use” is only detected reliably for the same bind address.
Security
Section titled “Security”There is no authentication and no TLS. Anyone who can reach the port can
stop services or shut the host down. The default bind is loopback
(127.0.0.1); binding anywhere else logs a warning at startup. Keep it on
loopback, or put it behind a proxy that authenticates. The single hook where an
auth check will go is request_authorized() in src/http/sig_http_server.c.
The editing endpoints also let anyone who can reach the port rewrite or delete
the project’s configuration files, so the same guidance applies. (A browser
sends a CORS preflight for a JSON PUT/PATCH, and without -o it is
refused, but a non-browser client is not affected by CORS at all.)
CORS is not an access control. Without -o, web pages cannot read responses,
but a blind cross-origin POST (stop, restart, /system/shutdown) from any
page in a browser that can reach the host still works, because there is no
authentication and no Origin check. -o <origin> additionally lets pages
from that origin read every response and make full cross-origin calls; -o '*'
does so for every origin, so any web page open in a browser that can reach the
host can read topology and properties and drive the API. Prefer naming the UI’s
origin, and keep the API on loopback or behind an authenticating proxy. An
Origin check in request_authorized() is the intended future hook.
Examples
Section titled “Examples”curl -s localhost:8080/systemcurl -s localhost:8080/servicescurl -s localhost:8080/services/test/instances/Simcurl -s -X POST localhost:8080/services/test/startcurl -s -X POST localhost:8080/services/test/restartcurl -s -X POST localhost:8080/system/shutdowncurl -s localhost:8080/capsulescurl -s -X PUT -H 'Content-Type: application/json' \\ -d '{"type":"simulator","attr_value":{"end":9,"interval_ms":250}}' localhost:8080/configurations/Sim2curl -s -X PATCH -H 'Content-Type: application/json' \\ -d '{"attr_value":{"end":99}}' localhost:8080/configurations/Sim2curl -s -X DELETE localhost:8080/configurations/Sim2For embedding without HTTP, or to build another transport, call the operations in
include/sig/mgmt/sig_mgmt.h directly; see the management API.