OPAL permissions

Security and access control for Plans

Reference for the OPAL permission system that controls what Plans can access and modify.

Security Note

OPAL's capability-based permission model means Plans must explicitly request the resources they need, and users review these requests before approval. This "principle of least privilege" protects your hardware from malicious or buggy Plans - a Plan that only requests sensors.distance can never accidentally (or intentionally) spin your motors.


Permission model

OPAL uses a capability-based security model:

  1. Plans declare required permissions
  2. User reviews and approves permissions
  3. Runtime enforces permission checks
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚     Plan        β”‚
    β”‚  (requests      β”‚
    β”‚   permissions)  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   Permission    β”‚
    β”‚   Checker       β”‚
    β”‚ (grants/denies) β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   Hardware/     β”‚
    β”‚   Resources     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Permission categories

Category Prefix Description
Sensors sensors.* Read sensor data
Actuators actuators.* Control motors, servos
Communication comms.* Send/receive messages
Storage storage.* Persist data
System system.* System operations
Network network.* Network access

Sensor permissions

sensors.read

Read any sensor value.

// Requires: sensors.read
var dist = sensors.distance.front
var heading = sensors.imu.heading

sensors.distance

Read distance sensors only.

// Requires: sensors.distance
var dist = sensors.distance.front
var left = sensors.distance.left

sensors.imu

Read IMU (orientation) sensors.

// Requires: sensors.imu
var heading = sensors.imu.heading
var pitch = sensors.imu.pitch

sensors.line

Read line following sensors.

// Requires: sensors.line
var on_line = sensors.line.center

sensors.camera

Access camera feed.

// Requires: sensors.camera
var frame = camera.capture()

sensors.gps

Access GPS location.

// Requires: sensors.gps
var lat = gps.latitude
var lon = gps.longitude

Actuator permissions

actuators.control

Control any actuator.

// Requires: actuators.control
drive(50, 50)
set_servo("arm", 90)

actuators.motors

Control DC motors only.

// Requires: actuators.motors
drive(50, 50)
set_motor("left", 60)

actuators.servos

Control servos only.

// Requires: actuators.servos
set_servo("head.pan", 90)
set_servo("head.tilt", 45)

actuators.relays

Control relay outputs.

// Requires: actuators.relays
set_relay("lights", true)

actuators.emergency_stop

Access emergency stop function.

// Requires: actuators.emergency_stop
emergency_stop()

Note: Emergency stop is always allowed regardless of other permissions for safety.


Communication permissions

comms.broadcast

Send broadcast messages.

// Requires: comms.broadcast
broadcast("status", {state: "idle"})

comms.send

Send direct messages to peers.

// Requires: comms.send
send_to("Worker A", "command", {action: "start"})

comms.receive

Receive messages from peers.

// Requires: comms.receive
on_message("task") { data ->
    execute(data)
}

comms.peers

Access peer information.

// Requires: comms.peers
var peer_list = peers.list()
var status = peers["Worker A"].status

Storage permissions

storage.read

Read stored data.

// Requires: storage.read
var config = storage.get("config")
var history = storage.get("patrol_history")

storage.write

Write data to storage.

// Requires: storage.write
storage.set("last_patrol", now())
storage.set("config", {speed: 50})

storage.delete

Delete stored data.

// Requires: storage.delete
storage.delete("old_data")
storage.clear()

System permissions

system.info

Access system information.

// Requires: system.info
var version = system.version
var uptime = system.uptime
var name = system.personality_name

system.battery

Access battery status.

// Requires: system.battery
var level = battery.level
var voltage = battery.voltage

system.time

Access time functions.

// Requires: system.time
var now = now()
var hour = time.hour

system.plans

Control other plans.

// Requires: system.plans
activate("PatrolPlan")
deactivate("OldPlan")
pause("BackgroundTask")

system.config

Modify system configuration.

// Requires: system.config
// Very restricted - rarely granted
config.set("network.port", 8125)

clockwork.write

Create, change, pause, resume and delete the stored schedules of the node: clockwork.put, clockwork.pause, clockwork.resume and clockwork.delete. clockwork.put asks once in each session. clockwork.delete asks at every call. See Clockwork.

clockwork.run

Fire a stored schedule now, once: clockwork.run-now. It asks at every call, because the job of the schedule starts at once.


Network permissions

network.local

Access local network.

// Requires: network.local
// Used for LAN discovery and communication

network.internet

Access external internet.

// Requires: network.internet
// Rarely granted - for cloud services
http.get("https://api.example.com/data")

web.fetch

web.fetch reads one web page or file into an attachment. It is unsafe, so in safe mode the chat shows the approval card before it runs.

The reason is not the network. It is that the URL usually comes from the model, not from you. Text inside an attachment, or inside a page already fetched, can name the next URL, and text from the conversation can ride in its query string. The approval card shows the URL so you can see where the request goes.

Two calls ask again, every time, in ask and safe mode:

Scope The call Also needs
web.fetch.large Consents to a body over the setting Size without asking, 2 MiB by default, up to 100 MiB. The card shows the size the host declared A person at this machine
web.fetch.local Reaches a host on your local network: loopback, a private address, a link-local address or a local name A person at this machine

A public page that redirects into your local network is refused whatever you approved.

all mode asks nothing. It is full trust in the model, for these two calls as for every other. A large download and a local test server are then fetched without a card. Choose safe if you want to see them first.

To change the size limit, see Give OPAL a reference β†’ Change the limits.

Two tasks need the owner tier, so they run only while the device owner is logged in: llm.permission.set, which sets the permission mode, and config.set on the opal schema, which changes these limits and the mode. A plan from another node cannot run them.


Declaring permissions

In plan header

#!opal-plan v1.0
# @permissions sensors.distance, actuators.motors
# @permissions comms.broadcast

plan MyPlan {
    // Plan code
}

In metadata.json

{
    "name": "My Plan",
    "permissions": [
        "sensors.distance",
        "actuators.motors",
        "comms.broadcast"
    ]
}

Permission groups

Common permission bundles:

basic

Minimal permissions for simple plans.

sensors.read
system.time

mobile

Permissions for mobile agents.

sensors.read
actuators.motors
system.battery

autonomous

Full autonomous operation.

sensors.read
actuators.control
comms.broadcast
storage.read
storage.write
system.info

admin

Administrative access (rarely used).

sensors.read
actuators.control
comms.*
storage.*
system.*

Permission UI

When importing a plan:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Permission Review                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                              β”‚
β”‚  Plan: Obstacle Avoider                      β”‚
β”‚  Author: PlanAuthor42                        β”‚
β”‚                                              β”‚
β”‚  This plan requests:                         β”‚
β”‚                                              β”‚
β”‚  βœ“ sensors.distance                          β”‚
β”‚    Read distance sensor values               β”‚
β”‚                                              β”‚
β”‚  βœ“ actuators.motors                          β”‚
β”‚    Control drive motors                      β”‚
β”‚                                              β”‚
β”‚  βœ“ system.battery                            β”‚
β”‚    Check battery level                       β”‚
β”‚                                              β”‚
β”‚  ⚠ Risk Level: LOW                          β”‚
β”‚    Only accesses sensors and motors          β”‚
β”‚                                              β”‚
β”‚  [Approve] [Deny] [Review Code]              β”‚
β”‚                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Risk levels

Level Description
LOW Read-only sensors, no network
MEDIUM Motor control, local storage
HIGH Network access, all actuators
CRITICAL System config, internet access

Runtime enforcement

Permission check

At runtime, each operation checks permissions:

Plan calls: drive(50, 50)
    ↓
Permission check: actuators.motors
    ↓
Granted? β†’ Execute
    ↓
Denied? β†’ Throw E501 Permission Denied

Error handling

try {
    drive(50, 50)
} catch error {
    if error.code == "E501" {
        log_error("No motor permission!")
        // Fallback behavior
    }
}

Trust levels and permissions

Permissions interact with trust levels:

Trust Level Max Permissions
Block None
Ignore None
Meet sensors.read
Handshake sensors.read, comms.receive
Trust All except system.config
Depend All

Remote control from a peer is limited by:

  1. Plan's declared permissions
  2. Peer's trust level
  3. User's approval

Best practices

Principle of least privilege

Request only what you need:

// Bad - requests everything
# @permissions actuators.control

// Good - requests only motors
# @permissions actuators.motors

Explain why

Document permission use:

# @permissions sensors.distance
#   Used for obstacle detection
# @permissions actuators.motors
#   Used for driving and avoidance

Handle denials gracefully

try {
    var dist = sensors.distance.front
} catch error {
    if error.code == "E501" {
        // Fall back to simpler behavior
        use_bump_sensors_only()
    }
}

Avoid permission: network.internet

Internet access should rarely be needed. Consider:

  • Local processing instead of cloud
  • Hub-mediated cloud access
  • Offline-first design

Security considerations

Plan signing

Plans from trusted sources can be signed:

# @signature sha256:abc123...
# @author verified:PlanAuthor42

Signed plans show verified author in permission dialog.

Sandboxing

Plans run in a sandbox:

  • Cannot access filesystem directly
  • Cannot execute system commands
  • Limited memory and CPU
  • Network access controlled

Audit logging

All permission-controlled actions are logged:

[12:34:56] Plan "Patrol" used sensors.distance
[12:34:56] Plan "Patrol" used actuators.motors

In this section
Topics
reference OPAL permissions security access-control
See also