Mobile gateway¶
This page describes how to start, pair, and expose the workstation-hosted mobile gateway.
The SASE mobile gateway is a workstation-hosted HTTP gateway for paired mobile clients. The phone is only a client: it pairs with the host, stores a bearer token, calls product-shaped SASE APIs, and subscribes to server-sent events. The gateway never exposes a generic file, shell, or RPC surface.
The implementation is split across repos:
saseowns user configuration, CLI startup, and lifecycle glue throughsase mobile gateway start.sase.integrations.mobile_notificationsis the stable Python facade used by the gateway host bridge to project local notifications, build attachment manifests, and execute generic gate or specialized question actions. The sibling_mobile_notification_*modules are internal implementation details.sase.integrations.mobile_agentsandsase.integrations.mobile_helpersare the fixed-operation bridge facades used by the Rust gateway to list/launch/kill/retry/fork agents and to expose Patch, macro, bead, and update helpers. The sibling_mobile_agent_*and_mobile_helper_*modules are internal implementation details.../sase-core/crates/sase_gatewayowns the Rust HTTP server, wire records, pairing/token storage, audit log, SSE event stream, and committed API contract snapshot.../sase-android/README.mdis the Android client handoff for build/test commands, fake-gateway coverage, foreground UX smoke checks, and Epic 7 limitations.docs/contribute/mobile-mvp-runbook.mdis the install/operate/security runbook for private APK distribution, Tailscale Serve remote access, push payload boundaries, troubleshooting, and rollback.
Start Locally¶
Install the local checkout first so the Python CLI and sibling Rust binaries are available:
just install-venv
cargo build -p sase_gateway --manifest-path ../sase-core/Cargo.toml
sase mobile gateway start uses -c/--command or mobile_gateway.command when one is
set. Otherwise it resolves sase_gateway from PATH, then from the running Python
environment's bin/ directory, then from the sibling ../sase-core debug or release
target. Use -c /path/to/sase_gateway only when you need to override that lookup.
Start the gateway from SASE:
sase mobile gateway start
When the effective built-in gateway service proc is available and enabled, this
command delegates ownership to the service host. It clears a boot-scoped stop, starts or
nudges the host, and returns instead of holding a foreground process. The gateway is
disabled by default and is enabled per machine in the overlay where it is wanted; enable
it for this machine and start it with:
sase service proc enable gateway
sase mobile gateway start
sase mobile gateway pair
The final command asks the already-running gateway for a new pairing challenge. Use
sase service proc show gateway, sase service proc logs gateway, and the Services tab
for lifecycle and output. sase service proc stop gateway stops it until the next host
boot; sase service proc disable gateway changes the durable machine override.
Otherwise, sase mobile gateway start retains the foreground path. It runs the Rust
gateway on 127.0.0.1:7629, waits for GET /api/v1/health, creates a one-time pairing
challenge, and prints:
Starting SASE mobile gateway at http://127.0.0.1:7629
Pairing code: 123456
Pairing ID: pair_abc123
Expires at: 2026-05-06T15:00:00Z
Keep this process running while mobile clients connect.
On this fallback path, keep that process running while clients connect and stop it with
Ctrl-C. An unavailable, disabled, or unconfigured gateway service proc also falls back
to this path.
Useful startup overrides:
sase mobile gateway start -p 7630
sase mobile gateway start -H /tmp/sase-mobile-state
sase mobile gateway start -c "../sase-core/target/debug/sase_gateway"
-H sets the SASE home root passed to the Rust gateway as --sase-home; gateway files
are then stored below that root in mobile_gateway/. The matching configuration keys
live under mobile_gateway:
mobile_gateway:
bind_address: "127.0.0.1"
port: 7629
state_dir: ""
allow_non_loopback: false
command: ""
agent_bridge_command: ""
helper_bridge_command: ""
push_provider: "disabled"
fcm_project_id: ""
fcm_service_account_json: ""
fcm_credential_env: ""
fcm_dry_run: false
push_timeout_seconds: 5
push_retry_limit: 1
startup_timeout_seconds: 10
Push Hints¶
Push delivery is disabled by default. When enabled, the gateway sends hint-only records derived from the same event stream used by SSE. Push payloads contain safe IDs, categories, reasons, and short display text; they do not contain bearer tokens, pairing codes, prompt bodies, response text, attachment contents, attachment tokens, or host paths. The mobile client must always fetch authenticated gateway state after a push wake or notification tap.
Local development can use the test provider, which records attempted delivery without leaving the process:
sase mobile gateway start -P test
FCM HTTP v1 is configured with non-secret pointers. Use either a service-account JSON path or an environment variable containing a short-lived bearer token or service-account JSON:
export SASE_FCM_CREDENTIAL='...'
sase mobile gateway start \
-P fcm \
-F my-firebase-project \
-E SASE_FCM_CREDENTIAL \
-D
Equivalent config:
mobile_gateway:
push_provider: "fcm"
fcm_project_id: "my-firebase-project"
fcm_service_account_json: "~/.config/sase/firebase-service-account.json"
fcm_credential_env: ""
fcm_dry_run: false
push_timeout_seconds: 5
push_retry_limit: 1
Only credential paths or environment-variable names are passed on the gateway command
line. Do not commit service-account JSON, Firebase project secrets,
google-services.json, signing keys, or local tailnet hostnames.
After pairing, a mobile client registers its provider token through the authenticated
session routes. provider_token is the opaque device/app token from the push provider;
treat it as device credential material and avoid committing or logging real values.
TOKEN="sase_mobile_example"
curl -sS -X POST http://127.0.0.1:7629/api/v1/session/push-subscriptions \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"schema_version": 1,
"provider": "fcm",
"provider_token": "opaque-fcm-token",
"app_instance_id": "pixel-9-app-instance",
"device_display_name": "Pixel 9",
"platform": "android",
"app_version": "0.1.0",
"hint_categories": ["notifications", "agents", "update"]
}'
Duplicate registrations with the same provider token update the existing subscription. Clients can list active subscriptions and revoke one by ID:
curl -sS http://127.0.0.1:7629/api/v1/session/push-subscriptions \
-H "Authorization: Bearer $TOKEN"
SUBSCRIPTION_ID="sub_example"
curl -sS -X DELETE "http://127.0.0.1:7629/api/v1/session/push-subscriptions/$SUBSCRIPTION_ID" \
-H "Authorization: Bearer $TOKEN"
Private Remote Access And Packaging¶
The MVP runbook covers debug APK installation, signed release APKs, Firebase-configured internal builds, versioning, Tailscale Serve setup, threat model, and rollback steps:
docs/contribute/mobile-mvp-runbook.md
Keep the gateway on 127.0.0.1 when using Tailscale Serve. Tailscale proxies the
loopback gateway inside the tailnet, so the SASE process does not need a LAN or public
bind. Use --allow-non-loopback / -L only for explicit LAN or tailnet address binds
during short smoke windows.
Pairing Flow¶
The local host starts pairing with POST /api/v1/session/pair/start. The response
contains a short-lived one-time code and a pairing_id; it does not contain a
long-lived credential. A mobile client finishes pairing by sending the code, the
pairing_id, and device metadata to POST /api/v1/session/pair/finish.
Example:
PAIRING_ID="pair_abc123"
PAIRING_CODE="123456"
curl -sS http://127.0.0.1:7629/api/v1/session/pair/finish \
-H 'Content-Type: application/json' \
-d "{
\"schema_version\": 1,
\"pairing_id\": \"$PAIRING_ID\",
\"code\": \"$PAIRING_CODE\",
\"device\": {
\"display_name\": \"Pixel 9\",
\"platform\": \"android\",
\"app_version\": \"0.1.0\"
}
}"
The finish response returns the bearer token exactly once:
{
"schema_version": 1,
"device": {
"schema_version": 1,
"device_id": "dev_example",
"display_name": "Pixel 9",
"platform": "android",
"app_version": "0.1.0",
"paired_at": "2026-05-06T15:00:00Z",
"last_seen_at": null,
"revoked_at": null
},
"token_type": "bearer",
"token": "sase_mobile_example"
}
Store the token on the client as a secret. Future authenticated requests use:
TOKEN="sase_mobile_example"
curl -sS http://127.0.0.1:7629/api/v1/session \
-H "Authorization: Bearer $TOKEN"
Only GET /api/v1/health, POST /api/v1/session/pair/start, and
POST /api/v1/session/pair/finish are unauthenticated. All other routes require this
bearer token.
Storage And Revocation¶
Gateway state is stored under <sase_home>/mobile_gateway/. With the default SASE home,
that is ~/.sase/mobile_gateway/.
- Paired devices live in
devices.json. - Mobile agent launch, kill, upload, and per-device project context state also lives under this directory.
- Raw bearer tokens are not written to disk; only SHA-256 token hashes are stored.
- Audit records append to
audit.jsonlwith device ID, endpoint, target ID when available, and outcome. Audit records avoid pairing codes and bearer tokens. - The Rust store includes a revocation primitive so future UI/API work can mark a device revoked. Revoked tokens fail authentication.
Network Exposure¶
The gateway binds to 127.0.0.1 by default. This is intentional: a local-only bind is
the safe desktop development and same-host test path.
For private remote access, prefer Tailscale Serve or an equivalent private tailnet path that terminates access control outside the gateway. Keep the gateway bound to loopback when possible, then serve the loopback endpoint through the private tailnet configuration. The remote-dispatch setup path uses the same loopback-plus-Serve exposure pattern; see the Remote Dispatch Runbook for target gateway supervision, bootstrap issuance, and controller enrollment.
LAN and public-interface binds are explicit opt-in only:
sase mobile gateway start -b 100.64.0.10 -p 7629 -L
Only use --allow-non-loopback / -L on trusted private networks. Do not expose the
gateway directly to the public internet. The MVP pairing flow uses auditable one-time
pairing codes and bearer tokens, not a hardened mTLS or SPAKE2 protocol.
Known MVP Limitations¶
- Notification reads are authoritative REST reads from the host JSONL store. Successful
gateway state/action mutations publish
notifications_changedSSE events, but passive file watching is intentionally out of the MVP. - Attachment downloads are capped by the gateway's configured max attachment bytes. Oversized, missing, directory, traversal, symlinked, or unknown-risk files appear in manifests without download tokens.
- Telegram remains supported through the shared pending-action compatibility path. If an Telegram installations with only compatibility pending-action JSON can be read, but Telegram is not yet required to use the shared store as its only source of truth.
- Agent project context is intentionally an MVP metadata and known-project cwd selector. It does not expose arbitrary host directory selection, and clients must use SASE prompt syntax for VCS refs rather than sending raw repo paths.
- Workflow helper routes are native helper APIs, not generic command execution. Patch, macro, and bead helpers are read-only; macro PDF generation is optional; and update completion events are opportunistic while status polling remains authoritative.
Related¶
- Mobile gateway API — host-bridge commands and contract.
- Remote dispatch — enroll a machine and launch with
%dispatch. - Mobile MVP runbook — install and operate.
- Integrations — other integration pages.
_[project]: A sase project is a named unit of work registered with SASE. _[workspace]: A sase workspace is a numbered clone of a project's primary repo, managed by the workspace store and tracked in that project's registry.json. _[agent]: A sase agent is an agent session or a single agent that does not belong to a session. _[prompt]: A prompt is the text SASE hands to a provider to start an agent turn. _[macro]: A macro is a reusable prompt triggered with #name in agent prompts. _[Patch]: A Patch is SASE's local unit of change. _[bead]: A bead is one tracked work item in SASE's git-native issue tracker. _[plan]: A plan is a persisted, approved artifact recording the intent behind agent work. _[TUI]: The TUI is SASE's terminal interface for running agents and reviewing results. _[epic]: An epic is an executable multi-phase plan run with sase bead work. _[gate]: A sase gate is a durable, command-backed request for one user decision, created when work must pause for the user. _[monitor]: A sase monitor is a monitor turn that runs one long command under a detached supervisor, so the command outlives the agent that started it. *[stitch]: A stitch is the lightweight ordered change record inside a Patch's STITCHES: section.