Soulacy Mobile¶
Soulacy Mobile is an always-on outbound channel for the native iOS and Android apps. Delivery first writes the complete result to the workspace's durable SQLite inbox. APNs and FCM are only discreet wake-up signals, so a missing or temporarily failing push service never loses an agent result.
Send a scheduled result¶
Destinations are all, user:<authenticated-subject>, or device:<installation-id>. The channel test endpoint defaults to all when no destination is supplied.
The iOS app registers its installation through /api/v1/mobile/devices, reads /api/v1/mobile/deliveries, and maintains read receipts per device. The System agent is filtered at the API boundary and is never returned to iOS.
Native alerts¶
Durable inbox delivery needs no additional configuration. A Personal gateway can wake a device directly through APNs. Mount the downloaded Apple .p8 key read-only into the container, then set:
SOULACY_MOBILE_APNS_TEAM_ID=YOUR_APPLE_TEAM_ID
SOULACY_MOBILE_APNS_KEY_ID=YOUR_APNS_KEY_ID
SOULACY_MOBILE_APNS_PRIVATE_KEY_FILE=/run/secrets/soulacy-apns-key.p8
Never commit the .p8 key or paste it into .env. The key file should be readable only by the service account. Direct APNs supports both sandbox and production device tokens and is the recommended Personal-edition configuration.
Alternatively, configure a trusted APNs relay:
SOULACY_MOBILE_PUSH_RELAY_URL=https://push.example.com
SOULACY_MOBILE_PUSH_RELAY_TOKEN=replace-with-a-secret
The gateway posts a token, APNs environment, bundle ID, delivery ID, and a soulacy://delivery/<id> deep link to POST <relay>/v1/push. Notification text is intentionally generic; the sensitive output is fetched from the authenticated gateway after the user opens the app.
Android (FCM)¶
The Android app (package dev.soulacy.android) registers through the same
POST /api/v1/mobile/devices endpoint with "platform": "android":
{"id": "<installation-id>", "name": "Pixel 9", "platform": "android",
"bundle_id": "dev.soulacy.android", "push_token": "<FCM registration token>",
"notifications_enabled": true}
platform is ios when omitted, and any other value is rejected. For
Android, bundle_id may be omitted but otherwise must be
dev.soulacy.android; push_token is the opaque FCM registration token (at
most 4096 characters, no whitespace) and is stored exactly as sent.
push_environment and live_start_token are ignored, and Live Activities
are never sent to Android devices.
The gateway sends through the FCM HTTP v1 API as a Google service account. Create a service-account key with the Firebase Cloud Messaging permission in the Firebase project of the Android app, mount the JSON file read-only into the container, then set:
SOULACY_MOBILE_FCM_CREDENTIALS_FILE=/run/secrets/soulacy-fcm-service-account.json
# Optional: defaults to project_id from the credentials file.
SOULACY_MOBILE_FCM_PROJECT_ID=your-firebase-project
Never commit the service-account file or paste it into .env. The gateway
signs its own OAuth2 assertion with the key, exchanges it at the file's
token_uri for an access token, and reuses that token until shortly before
it expires. FCM can be configured alongside APNs or a relay: iOS devices keep
using APNs or the relay, Android devices use FCM.
Every push is a data-only, high-priority message with no notification
block, so the app decides how to render it. Like APNs, it is only a wake-up:
the text is generic and the app fetches the content from the authenticated
gateway. The data map holds only strings:
| Key | Value |
|---|---|
category |
SOULACY_APPROVAL, SOULACY_DELIVERY, SOULACY_TRIGGER, ... |
title, body |
generic alert text |
deep_link |
for example soulacy://approval/<call_id> or soulacy://delivery/<id> |
thread_id |
grouping key |
time_sensitive |
"true" or "false" |
delivery_id |
present only for inbox deliveries |
Feature-specific keys ride along in the same map, for example call_id,
agent_id, session_id and tool on approvals. When FCM reports a token as
unregistered (HTTP 404 or UNREGISTERED), the gateway clears that device's
push token until the app registers a fresh one.
Lock-screen approvals¶
When an agent needs your approval for a tool call, every paired phone that
belongs to the same signed-in user receives a time-sensitive push in the
SOULACY_APPROVAL category with Approve and Deny actions. Deciding
from the lock screen calls the approvals API in the background; the app does
not have to open. Tapping the notification opens the approval instead. The
push carries only the agent name and tool name; the arguments are fetched from
the authenticated gateway.
Approvals still go through the same broker as the web, so a decision taken on the phone is visible everywhere, and an approval that has already been decided reports that instead of running twice.
Location triggers¶
Agents declared with trigger: location are listed for paired phones at
GET /api/v1/mobile/triggers?device_id=<installation-id>. The phone monitors
those regions and reports a crossing with
POST /api/v1/mobile/triggers/<agent-id>/fire. The gateway checks that the
event matches the trigger's on, that the phone is allowed by device, and
that the trigger's cooldown has passed, then starts the run and delivers
the reply to that phone through this channel. See the
SOUL.yaml reference for the block.
Live Activities¶
A running agent can sit on the lock screen and in the Dynamic Island as a Live Activity: the agent's name, what it is doing, how many steps it has taken, elapsed time, and Approve/Deny buttons whenever it is waiting on you. Nothing is rendered on the gateway; it pushes a small content state and the phone draws it.
Which runs appear:
- a background run (cron, webhook, location, another agent) as soon as it calls its first tool;
- any run, including interactive chat, the moment it needs an approval.
The phone sends an ActivityKit push-to-start token with its device
registration (live_start_token). The gateway uses it to begin the activity
even when the app is closed. Once the activity is on screen the phone reports
the per-activity update token with
POST /api/v1/mobile/activities (device_id, agent_id, session_id,
token, environment), and the gateway pushes progress, "waiting on you",
and the final state, then lets the phone dismiss it a few minutes later.
Routine progress is throttled and sent at low priority; waiting and finished
states are sent at once.
With direct APNs the pushes go to Apple with apns-push-type: liveactivity
and topic dev.soulacy.ios.push-type.liveactivity. A relay receives them at
POST <relay>/v1/live as {token, environment, bundle_id, event, priority,
payload} where payload is the exact APNs body. Turn the feature off per
phone under Settings → Lock screen; the phone then stops sending its
start token.
Siri and Shortcuts¶
The iOS app exposes App Intents so agents work without opening the app: Ask an agent, Remember something (adds an adaptive-memory fact), Approve the pending action, and Run an agent. They use the same paired credential and the same APIs as the app, so nothing new has to be configured on the gateway.
Apple Watch¶
The iPhone build carries a watch app. It pairs through the phone once:
the phone calls POST /pairing/tokens with no body, which issues a code
for the same subject, and the watch redeems it and keeps the resulting
managed key. From then on the watch calls GET /approvals,
POST /approvals/:id/approve or …/deny, GET /agents and POST /chat (voice mode)
directly, so it shows up as its own key in the household list and can be
revoked on its own. Nothing else has to be configured on the gateway.
Agent-built UI¶
An agent can draw a native card on a paired phone with the canvas.present
device command (through the mobile.invoke tool, once the phone has the
Soulacy canvas capability on). Besides the original title, body and
items, a card may carry typed components, rendered with native controls:
| Type | Fields |
|---|---|
text |
markdown |
checklist |
title, items: [{label, done}] |
form |
id, fields: [{name, label, kind: text\|number\|choice\|toggle\|date, options, required, default}], submit (button label) |
chart |
title, kind: bar\|line, labels, series: [{label, values}] |
metric |
label, value, unit, trend: up\|down\|flat |
A card with a form (or a submit label at the top level) keeps the command
running until the person submits or dismisses it, or the command expires,
so set expires_in_seconds on mobile.invoke (up to 900). The result then
carries answers keyed by field name and the state of every checklist:
{"presented": true, "submitted": true, "form_id": "pick",
"answers": {"option": "Beach", "notes": "leave early"},
"checklists": {"Pack": [{"label": "Passport", "done": true}]}}
Limits: 12 components, one form of up to 12 fields, checklists of up to 30 items, charts of up to 6 series and 60 points. The gateway refuses anything else with a message naming the vocabulary, so agents learn it from the error.
Household: one gateway, several people¶
Every paired phone signs in as a person. POST /api/v1/pairing/tokens
with no body pairs a phone as the caller; with {"name": "Priya", "role":
"operator"|"viewer"} an admin pairs a phone for someone else, whose
subject becomes a slug of the name (priya). The credential minted on
redeem carries that subject and role, so adaptive memory, approvals, the
inbox and device commands are all scoped to that person, exactly as they
are for the owner. GET /api/v1/pairing/members (admin) lists everyone
with a paired phone; revoking their keys through /admin/api-keys/:id
unpairs them. Companion keys minted before identities existed are the
owner's and are treated as such.