Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions docs/APP_OS_BOUNDARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# App and OS Boundary

OpenPhone has one product experience with two possible phone capability
profiles:

- **OpenPhone App** uses Android public APIs and user-granted roles and
permissions. It is the future Play-distributable profile.
- **OpenPhone OS** adds a privileged execution adapter, framework services,
SystemUI surfaces, keyguard configuration, and system policy enforcement.

Most product logic should be shared. The OS layer should remain a small,
auditable authority rather than becoming a second assistant implementation.

## Dependency Direction

```text
AI Home / generated surfaces / model and runtime adapters
|
v
PhoneToolGateway
/ \
v v
public Android app OpenPhone OS adapter
implementation (framework Binder client)
|
v
system_server / SystemUI
```

Code above `PhoneToolGateway` must not import `android.openphone` hidden APIs,
construct `FrameworkToolExecutor`, or decide that a privileged action is
authorized. It may request a registered tool, show review UI, and interpret a
structured result.

The gateway reports a stable profile and whether each tool is supported. A
smaller implementation must deny an unsupported tool explicitly; it must not
silently fall back to accessibility automation or a lower-risk action.

## Source Ownership

| Layer | Current source | Responsibility |
| --- | --- | --- |
| Portable product | `assistant/actions`, `model`, `orchestrator`, `runtime`, `surface`, and Compose UI | Conversation, model adapters, tool requests, adaptive UI, runtime sessions, and user review presentation |
| Phone boundary | `assistant/platform/PhoneToolGateway.java` | Stable app-to-phone execution contract and capability-profile seam |
| OpenPhone OS adapter | `assistant/platform/OpenPhoneOsToolGateway.java` and `assistant/agent/FrameworkToolExecutor.java` | Translate registered tools into the hidden OpenPhone framework manager and Android integrations |
| OS authority | `patches/frameworks_base`, Settings patches, SystemUI patches, and SELinux policy | Screen/input authority, secure confirmation, durable audit, island rendering, keyguard-aware behavior, and privileged state |

The current privileged APK is still the only build artifact. This boundary is
the first extraction step, not a claim that the APK can already be uploaded to
Google Play.

## Capability Placement

The public app profile can own AI Home as a user-selected launcher, immersive
window UI, voice and text interaction, generated surfaces, model/runtime
adapters, local history, public Android intents, and scheduled work that fits
normal Android limits.

The OS profile remains authoritative for:

- SystemUI-owned island and keyguard-safe rendering;
- non-secure lock-screen defaults and global hardware gestures;
- silent screen context and capture;
- cross-app input and task control;
- secure settings and other signature/privileged operations;
- tamper-resistant confirmation, policy, and audit storage;
- stronger direct-boot and always-available execution guarantees.

Public Android roles or APIs can cover some phone, SMS, notification, calendar,
contact, and launcher features. Their availability must be represented as
capabilities of the active gateway, never inferred from the package name.

## Extraction Sequence

Keep this work in reviewable follow-up changes:

1. Route runtime and adaptive-surface execution through `PhoneToolGateway`.
2. Split `FrameworkToolExecutor` into public-Android tools and OS-only tools,
with both implementations preserving the same result contract.
3. Move settings and durable stores behind app-owned configuration/storage
interfaces instead of reading arbitrary `Settings.Secure` keys in portable
packages.
4. Add separate app manifests and build targets: a public-SDK Play profile and
the current platform-signed `system_ext` profile.
5. Add a machine-readable capability matrix and run the same runtime/surface
contract suite against both profiles.

Until those steps are complete, changes must keep the existing OpenPhone OS
behavior green and must not weaken framework confirmation or audit paths to
make the app profile easier to implement.
6 changes: 6 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ The current repo implements the first OpenPhone product layer:
notifications, IME, recents, emergency UI, and system dialogs. OpenPhone
disables only the non-secure swipe keyguard by default; configuring a
pattern, PIN, or password restores Android's credential gate.
- A `PhoneToolGateway` boundary keeps model, runtime, and adaptive-surface
packages independent from hidden `android.openphone` APIs. The current
`OpenPhoneOsToolGateway` binds that portable contract to the framework-backed
executor; a future public-SDK implementation can expose a smaller supported
tool set without forking the product runtime. See
[APP_OS_BOUNDARY.md](APP_OS_BOUNDARY.md).
- Initial capability and policy config files. `scripts/check.sh` validates that
the assistant fallback `PolicyEngine` covers every capability in
`openphone_capabilities.json` with the same risk class.
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ it locally with:

- [Architecture](ARCHITECTURE.md) — system layers, OS services, agent
runtime, and current implementation boundaries.
- [App and OS Boundary](APP_OS_BOUNDARY.md) — shared product layers, the
phone-tool gateway, and the Play-app versus privileged-OS split.
- [Capabilities](CAPABILITIES.md) — named capabilities, risk levels, and
policy configuration.
- [Agent Runtime](AGENT_RUNTIME_V1.md) — background job model, scheduling,
Expand Down
6 changes: 6 additions & 0 deletions docs/releases/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ flow, and device support.

### Added

- An explicit `PhoneToolGateway` app/OS boundary: runtime and adaptive-surface
execution no longer import hidden OpenPhone framework APIs directly, and the
framework-backed implementation reports its platform profile and supported
tools through the shared contract.
- Dedicated OpenPhone Android Home activity with a calm black AI Home surface,
press-and-hold/release voice input, text fallback, accessible controls, and a
two-finger App Space gesture.
Expand Down Expand Up @@ -43,6 +47,8 @@ flow, and device support.
chrome stays hidden until a transient edge swipe, and the non-secure swipe
keyguard is disabled by default while configured credentials continue to
invoke Android's secure keyguard.
- OpenPhone Assistant development package bumped to `versionCode=173` /
`versionName=0.1.137-dev` for the app/OS execution boundary.
- Launcher3 remains installed for conventional app access but no longer
advertises itself as a competing Home activity in OpenPhone products.
- Tapping the compact idle OpenPhone island returns directly to AI Home; the
Expand Down
4 changes: 2 additions & 2 deletions overlay/packages/apps/OpenPhoneAssistant/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.openphone.assistant"
android:versionCode="172"
android:versionName="0.1.136-dev">
android:versionCode="173"
android:versionName="0.1.137-dev">
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
<uses-permission android:name="android.permission.GET_TASKS" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import org.openphone.assistant.orchestrator.OperatingMode;
import org.openphone.assistant.orchestrator.OrchestratorDecision;
import org.openphone.assistant.ota.OtaUpdateClient;
import org.openphone.assistant.platform.OpenPhoneOsToolGateway;
import org.openphone.assistant.platform.PhoneToolGateway;
import org.openphone.assistant.policy.AppCapabilityPolicy;
import org.openphone.assistant.surface.AdaptiveSurface;
import org.openphone.assistant.surface.AssistantOutput;
Expand Down Expand Up @@ -279,8 +281,8 @@ public void run() {
private boolean mComposeAdvancedVisible;
private ComposeStateCallbacks mComposeStateCallbacks;

OpenPhoneAgentManager agentManagerForSurfaces() {
return mAgentManager;
PhoneToolGateway phoneToolGatewayForSurfaces() {
return new OpenPhoneOsToolGateway(this, mAgentManager);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.openphone.assistant.model.OpenAiResponsesAgentAdapter;
import org.openphone.assistant.jobs.BackgroundJobReviewManager;
import org.openphone.assistant.jobs.OpenPhoneAgentJobScheduler;
import org.openphone.assistant.platform.OpenPhoneOsToolGateway;
import org.openphone.assistant.policy.AuditLog;
import org.openphone.assistant.policy.PolicyDecision;
import org.openphone.assistant.policy.PolicyEngine;
Expand Down Expand Up @@ -421,7 +422,8 @@ private boolean ensureRuntimeManagerReady() {
return false;
}
if (mRuntimeManager == null) {
mRuntimeManager = new RuntimeManager(this, mAgentManager);
mRuntimeManager = new RuntimeManager(
this, new OpenPhoneOsToolGateway(this, mAgentManager));
configureRuntimeCallback();
mRuntimeManager.start();
}
Expand All @@ -440,7 +442,8 @@ private void reloadRuntimeManager() {
return;
}
if (mRuntimeManager == null) {
mRuntimeManager = new RuntimeManager(this, mAgentManager);
mRuntimeManager = new RuntimeManager(
this, new OpenPhoneOsToolGateway(this, mAgentManager));
}
configureRuntimeCallback();
mRuntimeManager.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ object OpenPhoneHomeComposeHost {
val surfaceRepository = SurfaceRepository(activity)
val surfaceDispatcher = SurfaceActionDispatcher(
activity,
activity.agentManagerForSurfaces(),
activity.phoneToolGatewayForSurfaces(),
)
activity.setComposeStateCallbacks(object : AssistantActivityBackend.ComposeStateCallbacks {
override fun setTaskStatus(text: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.openphone.assistant.platform;

import android.content.Context;
import android.openphone.OpenPhoneAgentManager;

import org.json.JSONObject;
import org.openphone.assistant.agent.FrameworkToolExecutor;

/** OpenPhone OS implementation of the portable phone-tool boundary. */
public final class OpenPhoneOsToolGateway implements PhoneToolGateway {
public static final String PROFILE = "openphone_os";

private final OpenPhoneAgentManager mAgentManager;
private final FrameworkToolExecutor mToolExecutor;

public OpenPhoneOsToolGateway(Context context, OpenPhoneAgentManager agentManager) {
Context app = context.getApplicationContext();
mAgentManager = agentManager;
mToolExecutor = new FrameworkToolExecutor(app, agentManager);
}

@Override
public String profile() {
return PROFILE;
}

@Override
public boolean isAvailable() {
return mAgentManager != null;
}

@Override
public boolean supportsTool(String toolName) {
return isAvailable() && toolName != null && !toolName.trim().isEmpty();
}

@Override
public String startTask(String taskJson) {
if (!isAvailable()) {
return unavailable();
}
return mAgentManager.startTask(taskJson);
}

@Override
public String executeTool(String taskId, String toolName, JSONObject arguments) {
if (!isAvailable()) {
return unavailable();
}
return mToolExecutor.execute(taskId, toolName, arguments);
}

@Override
public String confirmAction(String pendingActionId, boolean approved) {
if (!isAvailable()) {
return unavailable();
}
return mAgentManager.confirmAction(pendingActionId, approved);
}

private static String unavailable() {
return "{\"error\":\"phone_platform_unavailable\"}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.openphone.assistant.platform;

import org.json.JSONObject;

/**
* Capability boundary between portable OpenPhone product logic and the phone
* implementation that executes tools.
*
* <p>The runtime and adaptive-surface layers depend only on this contract.
* OpenPhone OS supplies a framework-backed implementation; a Play-distributed
* build can supply a public-API implementation with a smaller capability set.
*/
public interface PhoneToolGateway {
/** Stable implementation profile exposed for diagnostics. */
String profile();

/** Whether this implementation can currently accept phone work. */
boolean isAvailable();

/** Whether this implementation supports the registered tool. */
boolean supportsTool(String toolName);

/** Starts a policy-scoped phone task and returns its JSON result. */
String startTask(String taskJson);

/** Executes one registered phone tool and returns its JSON result. */
String executeTool(String taskId, String toolName, JSONObject arguments);

/** Resolves an OS-owned pending action and returns its JSON result. */
String confirmAction(String pendingActionId, boolean approved);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.openphone.assistant.runtime;

import android.content.Context;
import android.openphone.OpenPhoneAgentManager;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.openphone.assistant.OpenPhoneNotificationController;
import org.openphone.assistant.platform.PhoneToolGateway;
import org.openphone.assistant.runtime.adapters.openclaw.OpenClawRuntimeAdapter;
import org.openphone.assistant.session.PhoneExecutionSession;
import org.openphone.assistant.session.PhoneSessionStore;
Expand All @@ -24,27 +24,29 @@ public final class RuntimeManager implements RuntimeConfirmationCallback {
private static final String TAG = "OpenPhoneRuntime";

private final Context mContext;
private final OpenPhoneAgentManager mAgentManager;
private final PhoneToolGateway mPhoneGateway;
private final RuntimeToolBridge mToolBridge;
private final PhoneSessionStore mSessionStore;
private final SurfaceRepository mSurfaceRepository;
private final List<RuntimeAdapter> mAdapters = new ArrayList<>();
private RuntimeCallback mRuntimeCallback;
private String mStatus = "disabled";

public RuntimeManager(Context context, OpenPhoneAgentManager agentManager) {
public RuntimeManager(Context context, PhoneToolGateway phoneGateway) {
mContext = context;
mAgentManager = agentManager;
mPhoneGateway = phoneGateway;
mSessionStore = new PhoneSessionStore(context);
mSurfaceRepository = new SurfaceRepository(context);
mToolBridge = new RuntimeToolBridge(context, agentManager, mSessionStore);
mToolBridge = new RuntimeToolBridge(context, phoneGateway, mSessionStore);
mToolBridge.setConfirmationCallback(this);
}

public synchronized void start() {
stopLocked();
RuntimeConfig config = RuntimeConfig.load(mContext);
if (mAgentManager == null) {
if (mPhoneGateway == null || !mPhoneGateway.isAvailable()) {
// Preserve the existing wire/status value while the Play profile
// is introduced behind the new gateway contract.
mStatus = "framework_unavailable";
return;
}
Expand Down Expand Up @@ -106,6 +108,8 @@ public synchronized String statusJson() {
.put("status", aggregateStatus)
.put("manager_status", aggregateStatus)
.put("lifecycle_status", mStatus)
.put("phone_platform", mPhoneGateway == null
? "unavailable" : mPhoneGateway.profile())
.put("updated_at_ms", System.currentTimeMillis())
.put("adapters", adapters)
.toString();
Expand Down
Loading
Loading