Permissions
Declare what you need in manifest.json. The user is asked once, at install,
and sees your list before agreeing.
Your bridge is built from what was granted, not what you asked for. A
declined permission is not an error at install time. Your plugin runs, and
those calls resolve false. Handle it.
| Permission | What the user is told | Grants |
|---|---|---|
voice |
"Speak" | voice.speak |
eyes |
"Move the eyes" | eyes.look, eyes.express |
led |
"Change the dock lights" | led.set, led.reset |
motor |
"Move the robot" | motor.pan, motor.tilt, motor.center |
camera |
"Use the camera" | camera.start/stop and face/hand events |
data |
"Read weather" | data.weather |
assistant |
"Ask the assistant" | assistant.ask |
microphone |
"Listen to you" | mic.listen |
open |
"Open other apps" | open.cab |
storage is not on this list and never will be. A plugin keeping its own
data under its own id is the same trust level as localStorage on a website:
you read back only what you wrote, no other plugin can see it, and there is no
network for it to leave by. Asking the user to approve that would be a prompt
with no decision in it, and prompts nobody can meaningfully answer are how
consent stops meaning anything for the permissions that do matter.
motor, camera, microphone and assistant are marked sensitive and
are presented more prominently. assistant is on that list because it spends
the user's own daily allowance and puts your text in front of the model on
their behalf, not because it can reach their data. Ask for them only if you use them. A permission you request and
never call costs you installs.
What camera is not
It does not give you the picture. You receive face and hand positions; there is no API that returns a frame, and the image never leaves the phone.
It also does not give you identity. You get how many faces are in front of the robot and where each one is, never who. Face recognition exists elsewhere in the app, behind its own separate consent, and it is deliberately not wired to plugins: a face embedding is biometric data, and handing it to third-party code is a different promise from the one on this screen.
Only the plugin that called camera.start() receives these events.
The user still sees the camera indicator while your plugin runs, so tell them why you need it.
Updates
An update that adds a permission does not inherit consent. The user is
asked again, and until they agree the new permission stays ungranted while the
rest keep working. Adding camera in v2 will not silently gain you a camera.
Users can change their mind
Permissions can be reviewed and revoked per plugin at any time in Settings.
Check info.get() at startup rather than assuming what you had last run.
eyes only does something in ambient mode
A session plugin is presented over the whole screen, and on this device the
screen is the face. hairobo.eyes.look() still runs and still returns true:
it moves a face that is underneath your page, invisible until you close.
So in session mode, animate your own DOM. Your page is the display.
In ambient mode you replace the idle face, and eyes is exactly the right
tool.
Uploading a session plugin that asks for eyes gets a warning, not an error.
There are legitimate reasons to set an expression that outlives your session.
But if you are animating eyes to be watched, check your mode first.
What assistant is not
It is not an API key, and it is not your budget.
The app makes the call with its own credentials on the signed-in user's
token, so the request is billed to the same daily allowance their ordinary
conversations use. You never see a key, and you can never obtain more assistant
than the user already has. When they run out, assistant.ask rejects with
daily limit reached. That is not a transient failure to retry around. Show
it and stop.
Your system string sets a persona or a topic. It reaches the model as
plugin-authored text inside the user turn, explicitly fenced and labelled, not
as a system instruction. So it cannot override the assistant's own rules, and
review reads it as carefully as it reads your code.
What microphone is not
It is not a recording. You receive what the user said, as text, and never audio.
mic.listen borrows the app's own listening turn, which has two consequences
worth designing around. The app's listening indicator and eye state are shown
while it runs, so you cannot listen without the user seeing it. And it only
starts when the robot is idle: ask while it is speaking or thinking and you
get null, not a queued turn.
null also comes back when the user simply said nothing. Silence is an
ordinary outcome, not an error: a tutor that treats it as a failure will be
wrong several times a session.
What open is not
It is not "open a link". You never give a URL. You name what you want, the app builds it and opens it.
That is not a formality. Your plugin has no network, and a link you chose would
be the way around it: anything you know could be put in the URL and read by
whoever opens it. So each thing you can open is added as a whole action that
the app performs on your behalf, and the app keeps what is private to the user.
open.cab('office') opens their cab app with the office set, and you never see
the address, the coordinates, or which app it was.
Opening another app takes the screen away from the user, so calls are limited
to one every few seconds; the extra ones resolve false.