Command authority
"A command completes before someone else modifies the state" has two levels, and serialised execution only solves the first. Across a sequence — read, decide, ramp, verify — an operator holds authority: the plant's arbitration concept, not a mutex. Local/remote, HMI-in-control, the teach pendant that owns the arm.
Any observable component can be held:
const oven = await client.component<Oven>('oven', 'ovenServer')
const grant = await oven.$acquire(60_000) // ttl in ms; a lease always expires
await oven.setMode('manual') // guarded methods now run for this peer
await oven.$release()$acquire when free is a grant; by the current holder, a renewal that extends the lease; by anyone else, a refusal naming who is in control. $acquire(ttl, { take: true }) is the break-in every plant panel has — atomic, visible, and a new generation — and who may use it is authorize()'s decision like every other call.
Only declared methods are gated
class Oven extends RpcComponent<OvenProps, OvenState> {
@rpc({ semantics: 'idempotent-command', requiresAuthority: true })
async setMode(mode: string) { ... }
@rpc({ semantics: 'idempotent-command' })
async stop() { ... } // deliberately undeclared: never behind a held lease
}That is the safety rule stated positively: an E-stop is written without the flag and is therefore structurally incapable of being blocked by a lease. Declaring requiresAuthority on a class that is not a component is refused at expose time — a safety-adjacent declaration that silently gates nothing would be the worst failure mode.
A call from a non-holder is refused with the NotInControl code, naming the holder — or saying "nobody is in control — $acquire it first". The check runs when the call arrives and again after any queue wait, because the wait is exactly where a takeover or an expiry lands.
Visible to everyone
Who holds the unit rides every component snapshot as authority: { holder?, expiresAt?, generation } — readable state, not a secret. Every change — grant, renewal, takeover, release, expiry — emits authorityChanged on the component with a reason, because a snapshot can say who is in control now but not whether the last holder released or was expired out.
The generation counter moves on every grant, takeover, release and expiry — but not on a holder's renewal, so extending a lease never fences out the holder's own queued commands. It is the within-epoch cousin of the topology layer's durable ownerEpoch; for fencing a call on that, see the owner fence.
An authorized observer can also ask the node-wide introspection surface for the current arbitration state:
const introspection = await client.proxy<Introspection>('msgrpc', 'ovenServer')
const authorities = await introspection.authorities()
// [{ namespace: 'oven', methods: ['setMode'], authority: { holder, expiresAt, generation } }]This query lists only components with methods that actually declare requiresAuthority. It stays separate from describe() because a lease is live state and a contract is structural cache data. The query is authorized like every other msgrpc method; refusal discloses no holder. The web console polls it only while the Security workspace or a relevant method form is visible, and shows unknown rather than a stale lease when it cannot be read. Poll answers are reconciled by monotonic generation and renewal expiry, and a read begun before an authority action is discarded when it eventually arrives; a successful acquire or renewal therefore cannot be overwritten by its older five-second poll.
On a known authority row, the console exposes acquire and release mine as ordinary declared DataProvider actions. Acquisition is bounded to a one-, five-, or fifteen-minute lease and doubles as renewal when the console already holds the component. Release remains the existing idempotent $release; takeover is not offered. Both calls run as the console's outbound identity and remain subject to the target's authorize() decision. An unknown or invented row id is not an alternate way to address a component.