# Control authority: leases, fences and the jog lane

> How rt-control admits one controller at a time, using an expiring lease, a session and generation fence, idempotent retries and a separate jog lane with its own deadlines.

URL: https://advancedmetalresearch.com/docs/concepts/control-authority
Section: RosieOS docs / Concepts
Last updated: 2026-10-10

A RosieOS cell has exactly one controller at a time. The pendant, the offline programming server, the motion servers, `rtctl` and your own code all ask rt-control for authority the same way, and they exclude each other. Authority is a **grant**: a session token plus a generation number (together, the **fence**), held under a lease that expires unless you renew it.

This page explains the model. The exact calls are in the [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http).

> [!WARNING] A grant lets you energise the drives and move the robot. The lease and the software Stop are not safety functions: the hardware E-stop is the only emergency stop, and RosieOS has no software E-stop. Keep the E-stop within reach, and read the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) before you arm a real cell.

## The lifecycle in one example

This Go program acquires a grant, keeps it alive in the background, enables and arms the cell, and then gives authority back. Every mutating call carries the fence automatically.

authority.go:

```go
package main

import (
	"context"
	"log"
	"time"

	"rosieos/rt-core/sdk/control"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	c, err := control.Dial("/run/rosie-rt-core/control.sock")
	if err != nil {
		log.Fatal(err)
	}
	defer c.Close()

	d, err := c.Describe(ctx) // also measures the round trip used to size the lease
	if err != nil {
		log.Fatal(err)
	}
	g, err := c.Acquire(ctx, "my-app", control.Binding{
		PairID: "cell-a", Revision: 1, // the pair rt-control was started with
		ConfigurationSHA256: d.ConfigurationSHA256, MachineSHA256: d.MachineSHA256,
	})
	if err != nil {
		log.Fatal(err) // e.g. control_already_owned
	}
	log.Printf("session generation=%d lease=%dms", g.Generation, g.LeaseMS)

	renewals, err := c.StartRenewal(ctx, 0) // 0 = every third of the lease
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		for r := range renewals {
			if r.Err != nil {
				log.Printf("renewal stopped: %v", r.Err) // authority is gone; stop producing motion
			}
		}
	}()

	if _, err := c.Enable(ctx, uint32(1)<<len(d.Axes)-1); err != nil {
		log.Print(err)
	}
	if _, err := c.Arm(ctx); err != nil {
		log.Print(err)
	}

	// ... motion ...

	if _, err := c.Stop(ctx); err != nil { // inhibits and bumps the generation
		log.Print(err)
	}
	if _, err := c.Release(ctx); err != nil { // stops renewal, then releases
		log.Print(err)
	}
}
```

The [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk) page covers these calls in detail. The [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client) has the same shape.

## The grant

`acquire` returns a `Grant`:

| Field | Meaning |
|---|---|
| `session` | A fresh, unpredictable token of 64 lowercase hex characters. Empty after release or expiry. |
| `generation` | A uint64 that increases on every Acquire and every Stop. |
| `lease_ms` | The effective lease: `min(requested_lease_ms, cell ceiling)`. It stays the same for the life of the session. |
| `deadline_host_ns` | When the lease runs out, in the cell host's `CLOCK_MONOTONIC` ns. |
| `stopping` | `true` while a Stop is draining. Renewal still succeeds, but grants no motion. |
| `controller`, `binding` | Your label and the binding you acquired with. |

`acquire` succeeds only when nobody else holds authority and your `binding` matches the deployment: the pair ID and revision rt-control was started with, and the machine digest from Describe. If you send `machine_sha256` it must match exactly, and there is no fallback to `configuration_sha256`. A mismatch is `authority_binding_mismatch`. If another session holds authority you get `control_already_owned`.

The connection itself is authenticated by the transport: file permissions on the local socket, or the client certificate on the [remote listener](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). A session belongs to the principal that acquired it. Using it from another principal, or from the local socket when it was acquired remotely, returns `session_principal_mismatch`.

## The fence

Every command after `acquire` carries `session` and `generation`. rt-control checks them on every call.

- **Motion needs the exact current generation.** Enable, Arm, Home, jog, Prepare and Start are refused with `control_session_stale` if the generation is not the current one, if the lease has expired, or while a Stop is in flight.
- **Stop increments the generation.** A live session gets its session back with the new generation, so any command still in flight under the old one is fenced off. You need a fresh Enable and Arm before moving again.
- **Renew and Stop accept older generations.** A non-zero generation that is not newer than the current one, from the same session, may renew (and learns the current fence from the reply) or stop. It can never move.
- **Stop works after expiry.** A session that has expired or been revoked can still send Stop, so a client that lost its lease can still inhibit. It cannot regain motion permission.
- **Restarts invalidate sessions.** A session from a previous core incarnation gets `daemon_restarted`. A well-formed session this rt-control process never issued (for example, one from before an adapter restart) gets `fence`.

Handles follow the same rule. A trajectory handle belongs to the generation that prepared it, and Stop, expiry and connection loss retire every handle. A retired handle can never start again.

## Lease timing

The cell's compiled configuration sets two ceilings, and Describe publishes both:

| Profile (`control.link_profile`) | Lease ceiling `max_grant_lease_ns` | Jog input age `max_jog_input_age_ns` |
|---|---|---|
| `lan` (the default) | 500 ms | 250 ms |
| `internet` (starting values, unverified) | 3 s | 750 ms |

A cell can override either value explicitly, up to 10 s for the lease and 2 s for the input age, and leases are whole milliseconds. The effective lease is the smaller of what you request and the ceiling. If you request nothing, you get 500 ms, still capped by the cell.

Renew at most every third of the effective lease, on a connection of its own, so Status polls and uploads cannot delay it. The Go and C++ clients measure the Describe round trip and refuse to start when the lease cannot cover at least three round trips plus the renewal interval; that refusal is typed `control_session_stale` (`LeaseTimingRejected`).

> [!NOTE] Longer is not safer. A longer lease means the robot keeps its last authority for longer after the operator's link drops. A longer input age lets a stale jog velocity apply for longer before it expires. Both are safety trade-offs, not qualified stop times.

## What ends authority

| Event | What happens | What you do next |
|---|---|---|
| `stop` | Outputs are inhibited immediately, execution, uploads, handles and jog are cancelled, and the generation increments. The session survives if it was still valid. | Enable and Arm again, then prepare new motion. |
| `halt` | A controlled deceleration to an enabled hold. The grant, Enable and Arm are kept; the active trajectory handle and jog generation are retired. | Start new motion from the held position, or `begin_jog` again. |
| `release` | Stop, then the session is cleared. | Acquire again when you need authority. |
| Lease expiry | rt-control notices within about 20 ms, emits `grant_expired`, retires every handle, cancels jog and inhibits outputs through the same Stop path. | Acquire again, Enable, Arm and upload anew. |
| `reset_fault` | A submitted reset retires the session, even when the core refuses the reset. | Acquire again after recovery. |
| Core or adapter restart | Sessions from before the restart are refused. | Describe again, then acquire. |

Neither a Stop receipt nor a Halt receipt proves the robot is at standstill. Confirm in Status.

## Idempotent retries

Mutating JSON calls can carry a `request_id`. Within a live session, rt-control keeps the last 256 outcomes: a retry with the same ID and the same payload joins the original request or replays its final reply, so a lost reply never causes a second Start. The same ID with a different payload is `request_id_conflict`. Release, expiry and restarts end the guarantee, and binary `.rdt` uploads are never deduplicated. The details are under [Idempotent retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries).

## The jog lane

Jogging runs on its own lane, separate from the JSON command path, so a stalled HTTP connection cannot hold a velocity in place.

1. `GET /v1/jog/clock` returns the host clock's incarnation.
2. `begin_jog` reserves jog mode for an axis mask and returns a **jog generation**. It carries the capture time and an absolute deadline of the first input.
3. Each velocity update is a 224-byte binary frame: a Unix datagram on `jog.sock`, or a WebSocket frame on the remote listener. It carries the session, grant generation, jog generation, a strictly increasing sequence, its own capture time and deadline, and the velocities in each axis's unit per second.
4. `end_jog` revokes input and the core ramps the jog to a hold.

Four rules keep jog input from outliving its operator:

- **Deadlines are never extended.** Renewing the grant does not freshen an input, and a refused frame never refreshes the previous one.
- **Input ages out.** A frame's deadline may be at most `max_jog_input_age_ns` after its capture (250 ms on LAN). When input stops arriving, the core ramps the jog down within each axis's jog acceleration, holds, and publishes `jog_expired`. If the ramp cannot stay within its bounds, the outputs are inhibited instead.
- **Old sessions cannot come back.** After Stop, expiry, a source loss or a Home or configuration epoch change, frames for the old session are refused as `wrong_identity`, even with fresh timestamps.
- **Only the newest input counts.** In a burst of datagrams only the newest valid frame is applied. Producers keep only their latest unsent sample and never replay a backlog.

Remote jog adds a clock calibration exchange, because the pendant's clock is not the cell's. That is covered in [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#remote-jog-over-websocket).

## One controller, many clients

Because authority is exclusive, clients on one cell lock each other out while they hold it: a desktop OLP session holding the grant keeps the pendant out, and the other way round. Observation is not exclusive. Anyone who can reach the socket can read Describe, Status, events and telemetry without a lease.

## Related pages

- [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http): `acquire`, `renew`, `release`, `stop`, `halt` and the jog calls.
- [The real-time core](https://advancedmetalresearch.com/docs/concepts/real-time-core): what the core checks before any motion starts.
- [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-authority): the authority reasons.

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `rt-core/adapters/rosie/control/controller.go:169-191 (restart fence)`
- `rt-core/adapters/rosie/control/controller.go:536-644 (acquire, binding match)`
- `rt-core/adapters/rosie/control/controller.go:686-796 (authorize, renew)`
- `rt-core/adapters/rosie/control/controller.go:1216-1384 (stop, finishStop)`
- `rt-core/adapters/rosie/control/controller.go:1385-1438 (expiry watcher, 20 ms)`
- `rt-core/adapters/rosie/control/reset_linux.go:22-88`
- `rt-core/adapters/rosie/control/halt_linux.go:25-79`
- `rt-core/adapters/rosie/control/handles.go:5-19`
- `rt-core/adapters/rosie/control/idempotence.go:12-111`
- `rt-core/adapters/rosie/control/remote_listener.go:14-86`
- `rt-core/adapters/rosie/control/jog_linux.go:26-135,259-396`
- `rt-core/protocol/application-v1.schema.json:1100-1104 (rules.control_timing)`
- `rt-core/protocol/control.json (fast_control.max_grant_lease_ns, max_jog_input_age_ns)`
- `rt-core/engine/jog.hpp:16-55`
- `rt-core/config/templates/machine.json:228-229`
- `rt-core/sdk/control/renewal.go:25-93`
- `rt-core/sdk/control/timing.go:33-80`
- `rt-core/sdk/control/client.go:78-112`
