Skip to content

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).

With sighost:

Terminal window
cmake -S . -B build -DSIG_NET=ON -DSIG_HTTP_SERVER=ON -DSIG_MGMT=ON
cmake --build build
build/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.
  • restart can answer 409 already_running when it loses a race with a concurrent start of 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 config documents are re-rendered from a parsed tree: a double keeps a . or exponent (5.0 stays a double) but its spelling can change (1e3 comes back as 1000.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.

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.
  • Format. Every response, including errors, is application/json.
  • Methods. GET reads state; POST performs an action; PUT, PATCH and DELETE edit the project (see Editing). Methods are case-sensitive. PUT and PATCH bodies are JSON (Content-Type: application/json, a Content-Length header, up to 8192 bytes); other endpoints ignore a body.
  • Paths. Segments may be percent-encoded. Splitting on / happens before decoding, so %2F inside 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 (see GET /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>.cfg file 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.
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
{
"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.

{
"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).

{
"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.

{
"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.

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.

{
"configurations": [
{ "name": "Count", "type": "counter" },
{ "name": "Sim", "type": "simulator" }
]
}

Sorted by name. type is the capsule the configuration configures.

{
"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.

{ "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.

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 is 400 bad_request.

  • PUT creates (201) or replaces (200) the whole document, using the formats in configuration.md. PATCH is a JSON Merge Patch (RFC 7386): objects merge, scalars and arrays replace. null is rejected with 422, so remove a key by PUTting 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, an execution entry naming a missing configuration, or a receiver naming a missing instance is 422 invalid_config; the message names 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.tmp and 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 on stop then start or restart. A stopped service is rebuilt immediately. Editing a configuration marks every service that uses it dirty.

  • DELETE of a service stops it first, then removes its definition and file. DELETE of a configuration that any service definition still uses is 409 configuration_in_use.

  • Renaming a configuration (POST /configurations/{name}/rename with {"name": "NewName"}) renames its file and rewrites the execution entries 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": true until 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} is 404, an invalid new name or body is 400, and a new name that already belongs to a configuration is 409 name_taken. The configuration is validated as it would be by a PUT. 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 }.

{ "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 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).

  • 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_ms expires; a long start/stop blocks the server while the service’s hooks run.
  • Limits: head 8192 bytes, request target 512 bytes, body 8192 bytes.
  • CORS. With cors_origin set, every response carries Access-Control-Allow-Origin, and OPTIONS on any path returns 204 with Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type and Access-Control-Max-Age: 600. Without it, OPTIONS is 405.
  • WebSocket upgrade hand-off. A module can register an upgrade prefix (sig_http_server_register_upgrade). A GET with Upgrade: websocket and Connection: Upgrade (names and tokens case-insensitive; Connection may be a list) on such a path is validated and the connection is handed to that module, which writes the 101 itself; 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-Version must be 13 and Sec-WebSocket-Key non-empty, else 400 bad_request; an Origin header, if present, must equal cors_origin (or cors_origin is *), else 403 forbidden_origin (so with no cors_origin any browser origin is refused; a client that sends no Origin is allowed). If the module cannot take the connection the reply is 503 unavailable. An upgrade request on a path with no upgrade prefix is routed like any other GET. The upgrade is as unauthenticated as the rest of the API (see Security): the Origin check only stops other web pages in a browser, not other clients.
  • On macOS/BSD, SO_REUSEADDR allows a bind of 127.0.0.1:P even if another process is listening on 0.0.0.0:P, so “address in use” is only detected reliably for the same bind address.

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.

Terminal window
curl -s localhost:8080/system
curl -s localhost:8080/services
curl -s localhost:8080/services/test/instances/Sim
curl -s -X POST localhost:8080/services/test/start
curl -s -X POST localhost:8080/services/test/restart
curl -s -X POST localhost:8080/system/shutdown
curl -s localhost:8080/capsules
curl -s -X PUT -H 'Content-Type: application/json' \\
-d '{"type":"simulator","attr_value":{"end":9,"interval_ms":250}}' localhost:8080/configurations/Sim2
curl -s -X PATCH -H 'Content-Type: application/json' \\
-d '{"attr_value":{"end":99}}' localhost:8080/configurations/Sim2
curl -s -X DELETE localhost:8080/configurations/Sim2

For embedding without HTTP, or to build another transport, call the operations in include/sig/mgmt/sig_mgmt.h directly; see the management API.