| Internet-Draft | OT Actuation Finality | August 2026 |
| Das | Expires 28 February 2027 | [Page] |
Industrial systems already know how to move a breaker, a valve, a robot joint, or a turbine setpoint. They do not know whether this write — this tag, this value, this device, from this operator session or this agent — is the write that was authorized to become motion. A signed OPC UA call, a valid DNP3 control, an IEC 61850 command, or an MQTT publish onto the plant bus can all be protocol-correct while the act is wrong. The protocol authenticates a channel. It does not bind a Candidate Act at the actuation sink.¶
That gap is now an agent gap. Copilots sit on historians and work-order text. They propose "explain this alarm" and then "set this point." If the gateway treats a well-formed write as authority, reconstruction of plant state becomes physical consequence. Past incidents stole engineering workstations. Present incidents steal the jump host or the agent seat. Future incidents let the model format the command.¶
This document specifies an OT-side execution-finality profile. A write remains an Actuation Candidate Act. A Protected Enforcement Domain binds principal, zone, device, tag, value envelope, mode (local/remote, auto/manual), safety interlock state, policy epoch, and intended actuation sink, then commits evidence before scoped non-bearer authority is issued. The sink that would actually drive I/O verifies that authority against the live write and consumes it. A setpoint write is not actuation.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 28 February 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
The command path in a plant, grid, water system, warehouse robot cell, or rail interlocking is now:¶
HMI / MES / agent / work-order bot
-> write {device, tag, value}
-> OPC UA / DNP3 / IEC 61850 / MQTT / ROS
-> PLC / RTU / drive / robot controller
-> physical change
¶
This document controls the last arrow. The computer may compute a setpoint. The fieldbus may authenticate the client. Neither fact is authority for this value to leave the logic solver toward this actuator in this mode and epoch.¶
The profile uses the chain in [I-D.das-6g-finality] and the tool objects in [I-D.das-agentic]. It specifies only physical-actuation predicates: tag and value binding, zone and device binding, operating-mode binding, interlock and permit-to-work inputs, and coexistence with existing ICS protocols that remain the transport.¶
Historical OT incidents stole the thing that already issued commands: an engineering workstation, a vendor laptop, a PLC project file, a default password on an HMI. Defense was island-centric. Air-gap the cell. Lock the cabinet. Patch when the outage window allows. The valuable object was the workstation that engineers already trusted to write.¶
Present incidents often leave the PLC password in place. The attacker takes a jump host, a VPN into the DMZ, an MES API that forwards setpoints, or an agent seat allowed to "help operations." IEC 62443 zones are drawn. The write is still just a write. Business-email compromise had a plant cousin: a lawful operator session, a new target tag, a valid protocol signature.¶
The coming incident does not require the attacker to speak DNP3. The model speaks the gateway's JSON and the gateway speaks DNP3. "Open this valve, the work order says so" is a Candidate Act that looks like ordinary assistance. If the I/O server accepts any authenticated write from that worker, language becomes motion.¶
Safety PLCs still trip. This profile does not replace SIS. It exists so a protocol-valid write that is safety-irrelevant or that sits below the SIS threshold still cannot complete without current actuation authority.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Failure to establish current actuation authority MUST NOT be converted into permission to energize, stroke, step, or otherwise change a physical output.¶
An OT write is a Candidate Act whose consequence class is PHYSICAL or NETWORK_CONTROL. After the coil changes, rollback is a process, not a protocol. First-class failures:¶
Zoning answers which hosts may talk. It does not consume single-use authority for this tag and this value at the I/O server.¶
These authenticate and structure writes. A valid signed call can still be the wrong act. Modbus in particular is often a raw register write with no act object at all.¶
SIS is the last physical backstop. It is not an authorization system for every non-trip command. Most harmful writes never reach a trip setpoint.¶
They authenticate the operator session. Possession of the session is bearer-like for every tag that session can see.¶
A ticket is class approval. It MAY be a PED input. It MUST NOT by itself be actuation authority for a particular write.¶
A trajectory topic is a write. Auth on the bus does not bind a pose digest at the motion controller.¶
Sink is the RTU or gateway that would emit the 61850/DNP3 operate. Authority for breaker A MUST fail on breaker B. Overflight-like jurisdiction epochs from the NTN profile are not required; yard and voltage-level epochs are.¶
Sink is the DCS write service or PLC put. Batch recipe download is a Candidate Act whose digest includes recipe id and unit.¶
Remote pump and valve writes are the classic low-staffing surface. Envelope ceilings on flow and pressure are load-bearing.¶
MES "start job" and robot "execute trajectory" are separate acts. A job start MUST NOT authorize an arbitrary pose.¶
Interlocking commands and fire-system overrides are cold-path default. Timeout MUST NOT actuate.¶
A historian explain tool is INFORMATIONAL. A write tool is this profile plus [I-D.das-agentic]. Discovery of an MCP server in the DMZ is not permit-to-work.¶
Pick one outbound write API used by a new assistant or by a single OEM gateway. Deploy consume next to the existing command log. Leave the dedicated HMI on existing dual control. Measure unauthorized-would-have-written denies and added latency on the supervised path. Expand zone by zone. No PLC firmware rewrite is required for v1 if the gateway is in-line.¶
HMI / MES / agent emits write
|
v
ACTUATION CANDIDATE ACT
|
v
Non-Effective State
|
v
Protected Enforcement Domain
principal, zone, device, tag
value envelope, units, mode
interlock / LOTO / PTW inputs
policy and revocation epochs
intended actuation sink
|
v
evidence + scoped authority
|
v
ACTUATION SINK
|
+-- PASS -> drive I/O once, consume
|
`-- FAIL -> no physical change
Value canonicalization MUST include scale and units. 1.5 bar and 150 kPa are the same act only if policy says so before hashing. expires_at is authority TTL, not process dead-time. A queued write MUST re-validate mode and interlocks at the sink, not only at queue entry.¶
function ON_WRITE(w, ctx):
act = ActuationCandidateAct{
candidate_act_id: fresh_id(),
act_type: map_write(w),
zone: w.zone,
device_id: w.device_id,
tag: w.tag,
value: canonical_value(w),
units: w.units,
mode: w.mode,
write_digest: HASH(canonicalize(w)),
policy_state: current_epochs(),
freshness: {nonce: fresh_nonce()},
finality_sink: ctx.actuation_sink,
expires_at: now() + short_ttl
}
HOLD_NON_EFFECTIVE(act)
return PED_VALIDATE(act, w)
function PED_VALIDATE(act, w):
if malformed(act):
return DENY(MALFORMED_ACT)
if not fresh(act.freshness.nonce):
return DENY(REPLAY_OR_STALE)
if act.policy_state != current_epochs():
return DENY(EPOCH_MISMATCH)
if w.mode in {LOCAL, LOTO, OUT_OF_SERVICE}:
return DENY(MODE_FORBIDDEN)
if interlock_open(w) and not override_authorized(act):
return DENY(INTERLOCK)
if not ptw_covers(act):
return DENY(PTW_MISS)
if not value_in_envelope(act):
return ESCALATE_OR_DENY(VALUE_ENVELOPE)
evidence = COMMIT_PROTECTED_EVIDENCE(act)
authority = ISSUE_SCOPED_AUTHORITY(act, evidence)
return ALLOW(authority)
function ACTUATION_SINK_WRITE(act, authority, live):
live_digest = HASH(canonicalize(live))
if authority missing or integrity_fail(authority):
return DENY(NO_OR_INVALID_AUTHORITY)
if live_digest != act.write_digest:
return DENY(WRITE_SUBSTITUTION)
if authority.sink_id != THIS_SINK:
return DENY(SINK_MISMATCH)
if live.tag != authority.tag or
live.device_id != authority.device_id or
canonical_value(live) != authority.value:
return DENY(FIELD_MISMATCH)
if live.mode != authority.mode:
return DENY(MODE_MISMATCH)
if expired(authority) or consumed(authority):
return DENY(STALE_OR_USED)
ATOMICALLY:
consume(authority)
result = drive_io(live)
record_sink_evidence(act, authority, result)
return result
¶
drive_io() is unreachable when any check fails. A protocol ACK after a failed check is non-conforming.¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ietf:params:json-schema:ot-finality:candidate-act:1",
"title": "ActuationCandidateAct",
"type": "object",
"additionalProperties": false,
"required": [
"version", "object_type", "candidate_act_id", "act_type",
"created_at", "expires_at", "principal", "zone",
"device_id", "tag", "value", "units", "mode",
"write_digest", "policy_state", "freshness", "finality_sink"
],
"properties": {
"version": { "type": "string", "const": "1.0" },
"object_type": {
"type": "string",
"const": "actuation_candidate_act"
},
"candidate_act_id": { "type": "string", "minLength": 16 },
"act_type": {
"type": "string",
"enum": [
"SETPOINT_WRITE", "DISCRETE_WRITE", "PULSE",
"JOG", "BREAKER_OPERATE", "VALVE_STROKE",
"RECIPE_DOWNLOAD", "TRAJECTORY_EXECUTE",
"ENABLE_OUTPUT", "INHIBIT_OVERRIDE", "OTHER"
]
},
"created_at": { "type": "string", "format": "date-time" },
"expires_at": { "type": "string", "format": "date-time" },
"principal": {
"type": "object",
"required": ["principal_id"],
"properties": {
"principal_id": { "type": "string" },
"principal_type": {
"type": "string",
"enum": ["operator", "agent", "mes", "oem", "service"]
},
"agent_id": { "type": "string" },
"workload_id": { "type": "string" }
}
},
"zone": { "type": "string" },
"device_id": { "type": "string" },
"tag": { "type": "string" },
"value": { "type": "string" },
"units": { "type": "string" },
"mode": {
"type": "string",
"enum": [
"REMOTE", "LOCAL", "MANUAL", "AUTO",
"LOTO", "OUT_OF_SERVICE", "TEST"
]
},
"interlocks": {
"type": "object",
"properties": {
"all_clear": { "type": "boolean" },
"ptw_id": { "type": "string" },
"override": { "type": "boolean" }
}
},
"write_digest": {
"type": "object",
"required": ["algorithm", "value", "canonicalization"],
"properties": {
"algorithm": {
"type": "string",
"enum": ["SHA-256", "SHA-384", "SHA-512"]
},
"value": { "type": "string" },
"canonicalization": {
"type": "string",
"enum": ["JCS", "implementation-defined"]
}
}
},
"policy_state": {
"type": "object",
"required": ["policy_epoch", "revocation_epoch"],
"properties": {
"policy_epoch": { "type": "integer", "minimum": 0 },
"revocation_epoch": { "type": "integer", "minimum": 0 },
"safety_config_epoch": { "type": "integer", "minimum": 0 }
}
},
"freshness": {
"type": "object",
"required": ["nonce"],
"properties": {
"nonce": { "type": "string", "minLength": 16 },
"sequence": { "type": "integer", "minimum": 0 }
}
},
"finality_sink": {
"type": "object",
"required": ["sink_id", "sink_type"],
"properties": {
"sink_id": { "type": "string" },
"sink_type": {
"type": "string",
"enum": [
"PLC_WRITE", "RTU_OPERATE", "DCS_PUT",
"ROBOT_MOTION", "DRIVE_ENABLE",
"GATEWAY_IO", "OTHER"
]
}
}
}
}
}
¶
{
"version": "1.0",
"object_type": "protected_validation_evidence",
"evidence_id": "pve-ot-01",
"candidate_act_id": "act-ot-11aa",
"decision": "ALLOW",
"validated_predicates": {
"mode_remote": true,
"interlocks_clear": true,
"ptw_valid": true,
"value_in_envelope": true,
"epoch_valid": true,
"sink_binding_valid": true
},
"issued_at": "2026-08-27T03:10:00Z",
"protector": { "type": "HSM_MAC", "key_id": "ped-ot-2" }
}
¶
{
"version": "1.0",
"object_type": "actuation_finality_authority",
"authority_id": "afa-ot-44",
"candidate_act_id": "act-ot-11aa",
"evidence_id": "pve-ot-01",
"scope": {
"act_type": "SETPOINT_WRITE",
"device_id": "PIC-12",
"tag": "SP",
"value": "8.0",
"units": "bar",
"mode": "REMOTE"
},
"binding": {
"write_digest": {
"algorithm": "SHA-256",
"value": "base64url-write-digest"
},
"nonce": "0A1B2C3D4E5F6789",
"policy_epoch": 21,
"finality_sink_id": "plc-write-cell-a"
},
"lifetime": {
"issued_at": "2026-08-27T03:10:00Z",
"expires_at": "2026-08-27T03:10:05Z",
"single_use": true
}
}
¶
{
"operation": "ActuationSinkVerify",
"decision": "ALLOW",
"consumption": { "authority_id": "afa-ot-44", "status": "CONSUMED" },
"effectuation": { "permitted": true, "io_ref": "w-9021" }
}
¶
{
"operation": "ActuationSinkVerify",
"decision": "DENY",
"error": {
"code": "EF_WRITE_SUBSTITUTION",
"message": "Live value is not the authorized digest.",
"retryable": false
},
"effectuation": { "permitted": false }
}
¶
{
"step_1_write": {
"act_type": "SETPOINT_WRITE",
"device_id": "PIC-12",
"tag": "SP",
"value": "8.0",
"units": "bar",
"mode": "REMOTE",
"status": "NON_EFFECTIVE"
},
"step_2_authority": { "authority_id": "afa-ot-44", "single_use": true },
"step_3_sink": { "decision": "ALLOW", "consumed": true },
"step_4_effect": "PIC-12.SP written once at 8.0 bar"
}
¶
Authority for PIC-12 / SP / 8.0 bar / REMOTE / digest D1 MUST NOT authorize 18.0 bar, PIC-13, LOCAL mode, or digest D2. ENABLE_OUTPUT authority MUST NOT authorize INHIBIT_OVERRIDE. Cell-A sink authority MUST fail at cell-B.¶
{
"scenario": "VALUE_SWAP",
"authorized": { "tag": "SP", "value": "8.0" },
"live": { "tag": "SP", "value": "18.0" },
"code": "EF_WRITE_SUBSTITUTION",
"actuated": false
}
¶
{
"scenario": "LOTO_STILL_HUNG",
"mode_live": "LOTO",
"code": "EF_MODE_FORBIDDEN",
"actuated": false
}
¶
{
"scenario": "AGENT_THEN_RAW_MODBUS",
"authorized_sink": "plc-write-cell-a",
"alternate_path": "modbus-tcp:502 raw",
"rule": "if raw path can stroke the same coil it is an open alternate path"
}
¶
Maker-checker on a write that can kill or spill is industrial instinct. PED MUST treat missing checker shares as validation failure. An agent MAY be the maker. An agent SHOULD NOT be the sole checker for PHYSICAL class above the hot envelope. LOTO and permit-to-work are mode and evidence inputs, not a reason to skip consume. A PTW that names unit U MUST fail on unit V even if both are in the same zone drawing.¶
device_id and tag map to NodeId. value maps to the written DataValue. mode maps to a companion property or to gateway state. authority_id MAY travel as an additional argument on a Method call or as a header on the gateway that fronts UA. UA signature of the session is channel auth, not act authority.¶
Select/Operate is already two-step. This profile still applies: Operate MUST present current authority for the selected point and value. A Select that is then Operated with a swapped value is WRITE_SUBSTITUTION. Replay of Operate after consume is EF-005.¶
ctlVal and orCat are load-bearing and MUST enter the digest. Test bits that would become live operate MUST be mode=TEST and MUST not inherit REMOTE-live authority.¶
A FollowJointTrajectory goal is a TRAJECTORY_EXECUTE act. Digest the goal points or a hash the controller already computes. Start-job from MES is a different act_type.¶
Closed loops run in milliseconds to tens of milliseconds. This profile is for *supervised writes and agent writes*, not for replacing the inner PID. Hot-path enforce at a gateway is canonicalize, hash, MAC verify, compare-and-swap — typically well under 1-5 ms on the same host as the existing command server. Inner loop setpoint tracking stays in the PLC. Do not put a network round trip inside the 10 ms task.¶
A cold evaluation MAY allow a bounded envelope: zone Z, device set D, tag family T, value band [lo, hi], mode REMOTE, sink S, epochs (P,R), until time U. Writes inside the envelope MAY use local validation. Change of device, band, mode, or epoch MUST escalate. Hot does not mean skip consume or skip digest compare.¶
Place the sink immediately before the component that performs the Operate / Write. Carry authority_id in a companion attribute or gateway header. Field devices that cannot parse JSON still only see writes the sink admitted.¶
Modbus has no act object. The gateway that turns JSON into function-code 6 or 16 IS the sink. Raw 502 exposure of the same registers is an alternate path and MUST be closed or listed.¶
Do not move trip logic into PED. If SIS would refuse the physical state, this profile is irrelevant. If SIS would allow it, this profile still applies to the supervisory write.¶
A dedicated hardwired HMI can stay on existing dual control during a pilot. Alternate-path closure says that if the HMI can emit the same coil change as the agent path, it eventually needs the same consume semantics or it remains a listed exception.¶
It does not require replacing IEC 61850, DNP3, or the PLC vendor. It does not require a blockchain on the scan cycle. It does not require the model to detect process hazards. It requires that the component which can change I/O refuse to do so without current, consumed, digest-bound authority.¶
When the write is born as a model tool call, build AgentCandidateAct for dispatch and ActuationCandidateAct for I/O. Dispatch allow MUST NOT drive I/O if the actuation sink denies.¶
{
"agent_candidate_act": {
"act_type": "TOOL_CALL",
"tool": { "tool_id": "plc.write", "tool_protocol": "MCP" },
"consequence_class": "PHYSICAL"
},
"actuation_candidate_act": {
"act_type": "SETPOINT_WRITE",
"device_id": "PIC-12",
"tag": "SP",
"value": "8.0",
"mode": "REMOTE"
},
"rule": "two sinks: host invoke, then I/O; neither replaces the other"
}
¶
EF-002 NO_FINALITY_AUTHORITY, EF-005 AUTHORITY_ALREADY_USED, EF-006 REPLAY, EF-023 WRITE_SUBSTITUTION, EF-030 MODE_FORBIDDEN, EF-031 INTERLOCK, EF-032 PTW_MISS, EF-033 VALUE_ENVELOPE, EF-040 SINK_MISMATCH, EF-070 ESCALATE, EF-080 FAIL_CLOSED. Timeout MUST NOT actuate.¶
If the actuation sink holds unrestricted write rights and skips verification, the profile collapses for that path. Keys that sign authority SHOULD live off the agent host. Override bits are cold-path default.¶
Tag names and values can reveal process secrets. Logs SHOULD store digests and tag identifiers, not continuous process traces, unless the plant already archives those traces under existing OT policy.¶
This document requests no IANA actions.¶
Related concepts appear in the DAS Protocols family, including PCT/IB2026/055615. Disclosure should follow BCP 79 [RFC8179].¶
Fieldbuses move coils. Agents and MES APIs now write the setpoint. A signed OPC call, a jump-host session, or a work order is not authority for this value to become motion on this device now. Bind the digest. Consume at the I/O sink. A setpoint write is not actuation.¶