@enfinitos/sdk-robotics
EnfinitOS reference SDK for the ROBOTICS substrate.
A robotic-fleet operator (Starship-style sidewalk delivery, a Nuro fleet, a warehouse VDA-5050 AGV fleet, a ROS 2 cobot deployment, a Cobalt security patrol) embeds this SDK so each robot can:
- Receive versioned behavioural policies from the EnfinitOS rights/policy plane.
- Acknowledge policies idempotently with a monotonic version.
- Report behavioural events (rule fires, violations, skips) back to the platform's audit ledger.
- Heartbeat telemetry (location, battery, speed, sensor health).
- Receive or raise emergency stops on a priority channel that never sits behind queued traffic.
- Survive transport drops with bounded backlog + exponential reconnection.
The SDK is fleet-agnostic: it speaks one canonical wire contract to EnfinitOS and exposes adapters for the four fleet patterns the team has surveyed (ROS 2, VDA 5050, Starship-style REST+WS, a generic-template adapter). Plug in your fleet's specific control plane in src/adapters/ — the policy/audit machinery is the same.
Platform-side counterpart. This SDK consumes the platform's/v1/robotics/connectWebSocket endpoint. That endpoint is a separate engineering item — it is not part of this SDK ship. The SDK targets the documented wire contract insrc/types.ts; the platform-side service to accept these connections, persist the audit stream, and dispatch policy pushes is tracked as its own tranche.
Architecture
┌──────────────────────────┐
│ EnfinitOS Platform │
│ (rights, policy, audit) │
└────────────┬─────────────┘
│ WebSocket
│ (JWT in handshake)
│
┌────────────── (1) policy_push ────▼─────────────┐
│ │
│ ┌────────────────────────────────────┐ │
│ │ EnfinitOSRobotClient │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Policy │ │Lifecycle │ │ │
│ │ │ Resolver │ │Controller│ │ │
│ │ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │Behaviour │ │Emergency │ │ │
│ │ │ Reporter │ │ Stop │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
│ ▲
(2) policy_ack │ │ (4) emergency_stop
(3) behavioural │ │ (priority channel —
event │ │ never queued)
(5) telemetry │ │
▼ │
┌──────────────────────────────────┴───────────────┐
│ Adapter (ROS 2 / VDA 5050 / │
│ Starship-style / custom) │
└──────────────────────────────────────────────────┘
│
▼
Fleet's own control plane
(publishers, instantActions,
diagnostic topics, etc.)
Research synthesis — why this shape
The SDK borrows from five existing patterns:
| Pattern source | What we adopted |
|---|---|
| ROS 2 managed-node lifecycle | Five-state machine (CONFIGURED / ACTIVE / SUSPENDED / ERROR / TERMINATED). ERROR must convalesce through SUSPENDED — ISO 10218. |
| VDA 5050 (Toyota / Linde / BMW) | Five-channel topology (order, instantActions, state, visualization, connection) inspired our separate priority channel for emergency stops. Our instantActions equivalent is sendImmediate(). |
| Starship Technologies (sidewalk delivery) | REST + WebSocket telemetry @ 1–5 Hz; remote-operator handoff via a state transition. Inspired REQUIRE_HUMAN_OPERATOR reporting. |
| Boston Dynamics Spot SDK | gRPC E-Stop service is never queued behind other traffic. We adopted that for emergencyStop(). |
| Nuro / Refraction AI / Cobalt | Rules-based geofencing + remote oversight ⇒ exclusion-zone reporting + forced boolean for safety-system overrides. |
Getting started
Install
pnpm add @enfinitos/sdk-robotics
Five-minute hello-world
import { EnfinitOSRobotClient } from "@enfinitos/sdk-robotics";
const client = new EnfinitOSRobotClient({
orgId: "org_acme",
robotId: "robot_001",
fleetId: "fleet_main",
authToken: process.env.ENFINITOS_JWT!,
apiBaseUrl: "https://api.enfinitos.com",
});
// 1) wire policy receipt
client.subscribePolicy(async (policy) => {
console.log(`policy v${policy.version} with ${policy.rules.length} rules`);
// TODO: load each rule into your control plane, then ack.
await client.acknowledgePolicy(policy.version);
});
// 2) wire emergency stop receipt
client.onEmergencyStopReceived(async (cmd) => {
console.error(`E-STOP from ${cmd.issuedBy}: ${cmd.reason}`);
// halt motion, transition to SUSPENDED.
});
// 3) connect
await client.connect();
// 4) heartbeat
client.startTelemetryHeartbeat({
intervalMs: 1000,
buildReport: () => ({
reportedAt: new Date().toISOString(),
location: { lat: 51.5, lng: -0.1 },
batteryPct: 87,
speedKph: 5.4,
headingDeg: 90,
state: client.getState(),
}),
});
Lifecycle states
| State | What it means | Reachable from |
|---|---|---|
CONFIGURED | Connected and authenticated; awaiting first policy. | (initial) |
ACTIVE | Has a policy and is executing missions. | CONFIGURED, SUSPENDED |
SUSPENDED | Paused (operator intervention, low battery, soft fault). | CONFIGURED, ACTIVE, ERROR |
ERROR | Hard fault. Must transition through SUSPENDED before re-activating. | (any non-terminal) |
TERMINATED | Terminal. Client must be reconstructed to reconnect. | (any non-terminal) |
The ERROR → SUSPENDED requirement is intentional: it matches ISO 10218 robotics-safety guidance, forcing an operator to acknowledge the fault before the robot can move again.
Policy update flow
PLATFORM ROBOT (SDK)
│ │
│ policy_push { version: 7, rules: […] } │
│ ─────────────────────────────────────────► │
│ │ ─┐ apply()
│ │ │ fan out to
│ │ │ subscribers
│ │ ◄┘
│ │
│ │ ─┐ application
│ │ │ decides ack
│ │ │ is safe
│ │ ◄┘
│ policy_ack { policyVersion: 7, │
│ robotState: "ACTIVE", ackedAt: … } │
│ ◄───────────────────────────────────────── │
The platform considers a policy "live" only after the robot acks. Out-of-order pushes (which happen on reconnect when the server flushes a buffered older version after the most recent one) are silently dropped by the resolver; stale ack attempts are also dropped client-side.
Behavioural rule kinds
The eleven BehaviourRule kinds the SDK speaks today (mirroring apps/api/src/modules/rights/contracts/scope.ts):
| Kind | Convenience helper | Notes |
|---|---|---|
YIELD_TO_PEDESTRIANS | reportYieldEvent() | sidewalk-delivery; the rule_fired event |
MAX_SPEED_KPH | reportSpeedExceedance() | rule_violated when observed exceeds limit |
EXCLUSION_ZONE | reportExclusionZoneEntry() | breach with forced boolean for safety overrides |
OPERATING_HOURS | generic reportBehaviouralEvent() | rule_skipped outside window |
REQUIRE_HUMAN_OPERATOR | generic | rule_fired on remote-operator handoff |
DONT_DISPLAY_WHILE_MOVING | generic | AUTOMOTIVE substrate cross-over |
DRIVER_ATTENTION_REQUIRED | generic | AUTOMOTIVE |
INTEGRATE_WITH_VEHICLE_DISTRACTION_SYSTEM | generic | AUTOMOTIVE |
PASSENGER_DISPLAY_ONLY | generic | AUTOMOTIVE |
REQUIRE_HANDS_FREE | generic | AUTOMOTIVE |
CUSTOM | generic | escape hatch for fleet-specific rules |
The four BehaviouralEventType values:
rule_fired— rule activated and complied with.rule_violated— rule could not be honoured (compliance audit).rule_skipped— not applicable.rule_unknown— firmware does not implement the rule. Reported so the platform never silently allows a policy a robot can't enforce.
Adapter pattern
Each fleet ships an adapter that implements four small interfaces in src/adapters/_template/spi.ts:
| Interface | Responsibility |
|---|---|
FleetCommandSink | apply EnfinitOS policy onto the fleet's native command vocabulary |
FleetEventSource | translate native fleet events into BehaviouralEvents |
FleetTelemetrySource | build TelemetryReports from native pose/battery/sensors |
FleetLifecycleHook | optional — react to lifecycle transitions |
Adapter skeletons are shipped for:
adapters/ros2.ts— ROS 2 (rclnodejs/rclpy), maps onto managed-node lifecycle, latched topics for policy publication.adapters/vda5050.ts— VDA 5050 v2,instantActionsfor most policy rules, MQTT broker injected.
Customer-specific shapes (Starship, Nuro, Cobalt) follow the same template; we ship the two with the broadest install base.
Telemetry pattern
The heartbeat is opt-in (startTelemetryHeartbeat(opts)). Recommended cadence: 1 Hz for sidewalk delivery and warehouse AGVs; up to 5 Hz for autonomous-vehicle telemetry. The platform throttles ingest above 10 Hz to protect the audit ledger.
Report shape (TelemetryReport) is intentionally small: location, battery, speed, heading, state, free-form sensorHealth. Keep sensorHealth keys stable across reports so the SRE dashboard's time series stay coherent.
Emergency stop semantics
- Priority channel. The SDK's
sendImmediate()bypasses the normal outbound queue. An emergency stop fires even if a policy ack or telemetry frame is mid-send. - Idempotent.
commandIdis the dedup key. The SDK drops duplicate incoming commands. - Never queued behind reconnect backoff. If the WS is down when the application calls
emergencyStop(), the message sits at the head of the priority queue and is the FIRST thing flushed when the socket re-opens. - Lifecycle. Robot-side
emergencyStop()auto-transitions to SUSPENDED. Inbound emergency stops do NOT auto-transition — the application controlling the robot decides what "halt" means and reports the state change back via the normal lifecycle channel.
Sample integration — minimal viable robot
import { EnfinitOSRobotClient } from "@enfinitos/sdk-robotics";
async function main() {
const client = new EnfinitOSRobotClient({
orgId: "org_acme",
robotId: "robot_001",
fleetId: "fleet_main",
authToken: process.env.ENFINITOS_JWT!,
apiBaseUrl: "https://api.enfinitos.com",
});
client.subscribePolicy(async (policy) => {
console.log(`got policy v${policy.version}`);
await client.acknowledgePolicy(policy.version);
});
await client.connect();
// one heartbeat
await client.reportTelemetry({
reportedAt: new Date().toISOString(),
location: { lat: 51.5, lng: -0.1 },
batteryPct: 87,
speedKph: 0,
headingDeg: 90,
state: client.getState(),
});
// one behavioural event
await client.reportYieldEvent({
occurredAt: new Date().toISOString(),
location: { lat: 51.5, lng: -0.1 },
detectedPedestrians: 1,
closestDistanceM: 1.8,
});
await new Promise((r) => setTimeout(r, 500));
await client.disconnect();
}
main();
What's SDK-side vs platform-side
| Concern | This SDK | Platform tranche (separate) |
|---|---|---|
Wire protocol (WireMessage) | ✅ defined here | ✅ implemented by platform-side server |
/v1/robotics/connect WS endpoint | — | ⏳ separate work item |
| Audit-log persistence | — | ⏳ separate work item |
| JWT issuance | — | reuses the existing /auth/* plane |
| Policy authoring UI | — | already shipped in apps/web/.../compose |
| Adapters (ROS 2, VDA 5050, custom) | ✅ skeletons | (customer-owned) |
See also
apps/api/src/modules/rights/contracts/scope.ts—BehaviourRulesource-of-truth on the platform side.packages/adapters/_template— sibling DOOH adapter SPI with the same shape.docs/launch/substrate-readiness-matrix.md— ROBOTICS substrate readiness tracker.