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.distancecan never accidentally (or intentionally) spin your motors.
Permission model
OPAL uses a capability-based security model:
- Plans declare required permissions
- User reviews and approves permissions
- 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.
allmode 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. Choosesafeif 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:
- Plan's declared permissions
- Peer's trust level
- 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