From 59ba9f6585febec32c62eed423614d41b1a7dfe9 Mon Sep 17 00:00:00 2001 From: Adam Cohen Hillel Date: Mon, 20 Jul 2026 19:01:11 -0700 Subject: [PATCH] refactor: isolate app and OS execution --- docs/APP_OS_BOUNDARY.md | 90 +++++++++++++++ docs/ARCHITECTURE.md | 6 + docs/README.md | 2 + docs/releases/CHANGELOG.md | 6 + .../OpenPhoneAssistant/AndroidManifest.xml | 4 +- .../assistant/AssistantActivityBackend.java | 6 +- .../assistant/OpenPhoneAssistantService.java | 7 +- .../assistant/OpenPhoneHomeComposeHost.kt | 2 +- .../platform/OpenPhoneOsToolGateway.java | 64 +++++++++++ .../assistant/platform/PhoneToolGateway.java | 31 ++++++ .../assistant/runtime/RuntimeManager.java | 16 ++- .../assistant/runtime/RuntimeToolBridge.java | 46 +++++--- .../surface/SurfaceActionDispatcher.java | 7 +- scripts/check.sh | 5 + scripts/validate-app-os-boundary.mjs | 103 ++++++++++++++++++ 15 files changed, 364 insertions(+), 31 deletions(-) create mode 100644 docs/APP_OS_BOUNDARY.md create mode 100644 overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/OpenPhoneOsToolGateway.java create mode 100644 overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/PhoneToolGateway.java create mode 100644 scripts/validate-app-os-boundary.mjs diff --git a/docs/APP_OS_BOUNDARY.md b/docs/APP_OS_BOUNDARY.md new file mode 100644 index 0000000..bed6f76 --- /dev/null +++ b/docs/APP_OS_BOUNDARY.md @@ -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. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 168723e..0e917c7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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. diff --git a/docs/README.md b/docs/README.md index 38ef2ed..7ba38b9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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, diff --git a/docs/releases/CHANGELOG.md b/docs/releases/CHANGELOG.md index 50498db..7abca51 100644 --- a/docs/releases/CHANGELOG.md +++ b/docs/releases/CHANGELOG.md @@ -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. @@ -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 diff --git a/overlay/packages/apps/OpenPhoneAssistant/AndroidManifest.xml b/overlay/packages/apps/OpenPhoneAssistant/AndroidManifest.xml index 86decc9..a271971 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/AndroidManifest.xml +++ b/overlay/packages/apps/OpenPhoneAssistant/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="173" + android:versionName="0.1.137-dev"> diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java index 819b049..190d756 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java @@ -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; @@ -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 diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneAssistantService.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneAssistantService.java index 7c1f0db..0ea6eac 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneAssistantService.java +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneAssistantService.java @@ -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; @@ -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(); } @@ -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(); diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneHomeComposeHost.kt b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneHomeComposeHost.kt index 0732daf..58a7f09 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneHomeComposeHost.kt +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/OpenPhoneHomeComposeHost.kt @@ -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) { diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/OpenPhoneOsToolGateway.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/OpenPhoneOsToolGateway.java new file mode 100644 index 0000000..e461a86 --- /dev/null +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/OpenPhoneOsToolGateway.java @@ -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\"}"; + } +} diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/PhoneToolGateway.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/PhoneToolGateway.java new file mode 100644 index 0000000..ec5c358 --- /dev/null +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/PhoneToolGateway.java @@ -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. + * + *

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); +} diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeManager.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeManager.java index 020a169..2cbecf1 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeManager.java +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeManager.java @@ -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; @@ -24,7 +24,7 @@ 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; @@ -32,19 +32,21 @@ public final class RuntimeManager implements RuntimeConfirmationCallback { 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; } @@ -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(); diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeToolBridge.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeToolBridge.java index 9b113e2..b24ce80 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeToolBridge.java +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeToolBridge.java @@ -1,14 +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.actions.ToolCatalog; -import org.openphone.assistant.agent.FrameworkToolExecutor; +import org.openphone.assistant.platform.PhoneToolGateway; import org.openphone.assistant.session.PhoneExecutionSession; import org.openphone.assistant.session.PhoneSessionStore; @@ -29,8 +28,7 @@ public final class RuntimeToolBridge { private static final String TAG = "OpenPhoneRuntime"; private static final int MAX_COMPLETED_IDEMPOTENCY_RESULTS = 128; - private final OpenPhoneAgentManager mAgentManager; - private final FrameworkToolExecutor mToolExecutor; + private final PhoneToolGateway mPhoneGateway; private final PhoneSessionStore mSessionStore; private final Map mTaskIdsByRuntimeSession = new HashMap<>(); private final Map mPendingConfirmations = @@ -55,14 +53,13 @@ public Thread newThread(Runnable runnable) { }); private RuntimeConfirmationCallback mConfirmationCallback; - public RuntimeToolBridge(Context context, OpenPhoneAgentManager agentManager) { - this(context, agentManager, new PhoneSessionStore(context)); + public RuntimeToolBridge(Context context, PhoneToolGateway phoneGateway) { + this(context, phoneGateway, new PhoneSessionStore(context)); } - public RuntimeToolBridge(Context context, OpenPhoneAgentManager agentManager, + public RuntimeToolBridge(Context context, PhoneToolGateway phoneGateway, PhoneSessionStore sessionStore) { - mAgentManager = agentManager; - mToolExecutor = new FrameworkToolExecutor(context, agentManager); + mPhoneGateway = phoneGateway; mSessionStore = sessionStore == null ? new PhoneSessionStore(context) : sessionStore; } @@ -137,6 +134,23 @@ private RuntimeToolResult executeInternal(RuntimeToolRequest request, logResult(request, result); return result; } + if (!mPhoneGateway.supportsTool(request.tool())) { + JSONObject details = new JSONObject(); + try { + details.put("tool", request.tool()) + .put("phone_platform", mPhoneGateway.profile()); + } catch (JSONException ignored) { + } + RuntimeToolResult result = RuntimeToolResult.denied( + request.requestId(), + "tool_not_supported_by_phone_platform", + "The active phone platform does not support this tool.", + details, + auditId); + markSessionForResult(request, result); + logResult(request, result); + return result; + } RuntimeToolResult completed = completedResultFor(request, auditId); if (completed != null) { markSessionForResult(request, completed); @@ -196,9 +210,9 @@ private RuntimeToolResult executeInternal(RuntimeToolRequest request, } markSession(request, "running"); - String rawResult = mToolExecutor.execute(taskId, request.tool(), params); + String rawResult = mPhoneGateway.executeTool(taskId, request.tool(), params); if (approvedMutation) { - rawResult = confirmFrameworkActionIfNeeded(rawResult); + rawResult = confirmPhoneActionIfNeeded(rawResult); } RuntimeToolResult result = normalizeToolResult(request, rawResult, auditId, taskId); if (approvedMutation) { @@ -323,7 +337,9 @@ private RuntimeConfirmationResolution timeoutPendingConfirmationLocked( } private RuntimeToolResult validate(RuntimeToolRequest request, String auditId) { - if (mAgentManager == null) { + if (mPhoneGateway == null || !mPhoneGateway.isAvailable()) { + // Keep the established result code stable during this + // dependency-only refactor. return RuntimeToolResult.error(request.requestId(), "framework_unavailable", "OpenPhone framework service is not available."); } @@ -388,7 +404,7 @@ private String startRuntimeTask(RuntimeToolRequest request) { .put("approved_capabilities", new JSONArray() .put("tasks.observe") .put("screen.read.visible")); - JSONObject response = parseObject(mAgentManager.startTask(task.toString())); + JSONObject response = parseObject(mPhoneGateway.startTask(task.toString())); return response.optString("task_id", ""); } catch (JSONException | RuntimeException e) { Log.w(TAG, "runtime task start failed runtime=" + request.runtime() @@ -397,7 +413,7 @@ private String startRuntimeTask(RuntimeToolRequest request) { } } - private String confirmFrameworkActionIfNeeded(String rawResult) { + private String confirmPhoneActionIfNeeded(String rawResult) { JSONObject parsed = parseObject(rawResult); String pendingActionId = findStringRecursive(parsed, "pending_action_id"); if (pendingActionId.isEmpty() || "null".equals(pendingActionId)) { @@ -409,7 +425,7 @@ private String confirmFrameworkActionIfNeeded(String rawResult) { return rawResult; } try { - return mAgentManager.confirmAction(pendingActionId, true); + return mPhoneGateway.confirmAction(pendingActionId, true); } catch (RuntimeException e) { return errorJson("framework_confirmation_failed", e.getClass().getSimpleName()); } diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/surface/SurfaceActionDispatcher.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/surface/SurfaceActionDispatcher.java index cdbb46e..1a61c03 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/surface/SurfaceActionDispatcher.java +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/surface/SurfaceActionDispatcher.java @@ -1,12 +1,12 @@ package org.openphone.assistant.surface; import android.content.Context; -import android.openphone.OpenPhoneAgentManager; import org.json.JSONException; import org.json.JSONObject; import org.openphone.assistant.actions.ActionRegistry; import org.openphone.assistant.actions.ToolCatalog; +import org.openphone.assistant.platform.PhoneToolGateway; import org.openphone.assistant.runtime.RuntimeIdentity; import org.openphone.assistant.runtime.RuntimeConfirmationResolution; import org.openphone.assistant.runtime.RuntimeToolBridge; @@ -32,12 +32,13 @@ public final class SurfaceActionDispatcher { private final Context mContext; private final Map mPending = new HashMap<>(); - public SurfaceActionDispatcher(Context context, OpenPhoneAgentManager agentManager) { + public SurfaceActionDispatcher(Context context, PhoneToolGateway phoneGateway) { Context app = context.getApplicationContext(); mContext = app; mRepository = new SurfaceRepository(app); mEvents = new SurfaceEventLog(app); - mToolBridge = agentManager == null ? null : new RuntimeToolBridge(app, agentManager); + mToolBridge = phoneGateway == null || !phoneGateway.isAvailable() + ? null : new RuntimeToolBridge(app, phoneGateway); } public synchronized SurfaceActionResult invoke(String surfaceId, int revision, diff --git a/scripts/check.sh b/scripts/check.sh index 524ec1b..aff6edd 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -19,6 +19,7 @@ required=( docs/README.md docs/AI_FIRST_ENGINEERING.md docs/AGENT_RUNTIME_V1.md + docs/APP_OS_BOUNDARY.md docs/ARCHITECTURE.md docs/BUILD.md docs/CAPABILITIES.md @@ -96,6 +97,7 @@ required=( scripts/pull-latest-trajectory.sh scripts/check-runtime-protocol.sh scripts/validate-background-review-contract.mjs + scripts/validate-app-os-boundary.mjs scripts/validate-home-shell-contract.mjs scripts/validate-island-contract.mjs scripts/validate-surface-contract.mjs @@ -217,6 +219,8 @@ required=( overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/model/LocalHeuristicModelAdapter.java overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/model/OpenAiRealtimeAdapter.java overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/ota/OtaUpdateClient.java + overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/PhoneToolGateway.java + overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/OpenPhoneOsToolGateway.java overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/policy/AppCapabilityPolicy.java patches/frameworks_base/0001-OpenPhone-add-agent-manager-framework-service.patch patches/frameworks_base/0002-OpenPhone-add-foreground-context-and-audit-mediation.patch @@ -1143,6 +1147,7 @@ fi "$root/scripts/check-runtime-protocol.sh" node "$root/scripts/validate-surface-contract.mjs" node "$root/scripts/validate-background-review-contract.mjs" +node "$root/scripts/validate-app-os-boundary.mjs" node "$root/scripts/validate-home-shell-contract.mjs" node "$root/scripts/validate-island-contract.mjs" "$root/scripts/check-assistant-java.sh" diff --git a/scripts/validate-app-os-boundary.mjs b/scripts/validate-app-os-boundary.mjs new file mode 100644 index 0000000..487de58 --- /dev/null +++ b/scripts/validate-app-os-boundary.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const assistant = path.join( + root, + "overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant", +); + +function read(relative) { + return fs.readFileSync(path.join(root, relative), "utf8"); +} + +function fail(message) { + throw new Error(message); +} + +function sourceFiles(relativeDirectory) { + const files = []; + function visit(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + visit(entryPath); + } else if (entry.isFile() + && (entry.name.endsWith(".java") || entry.name.endsWith(".kt"))) { + files.push(entryPath); + } + } + } + visit(path.join(assistant, relativeDirectory)); + return files; +} + +const portableDirectories = ["actions", "model", "orchestrator", "runtime", "surface"]; +const forbiddenPortableMarkers = [ + "import android.openphone.", + "FrameworkToolExecutor", + "OpenPhoneOsToolGateway", +]; + +for (const directory of portableDirectories) { + for (const file of sourceFiles(directory)) { + const source = fs.readFileSync(file, "utf8"); + for (const marker of forbiddenPortableMarkers) { + if (source.includes(marker)) { + fail(`${path.relative(root, file)} crosses the app/OS boundary via ${marker}`); + } + } + } +} + +const contract = read( + "overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/PhoneToolGateway.java", +); +for (const marker of [ + "String profile()", + "boolean isAvailable()", + "boolean supportsTool(String toolName)", + "String startTask(String taskJson)", + "String executeTool(String taskId, String toolName, JSONObject arguments)", + "String confirmAction(String pendingActionId, boolean approved)", +]) { + if (!contract.includes(marker)) { + fail(`PhoneToolGateway is missing ${marker}`); + } +} +if (contract.includes("android.openphone")) { + fail("PhoneToolGateway must remain independent of hidden OpenPhone APIs"); +} + +const osGateway = read( + "overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/platform/OpenPhoneOsToolGateway.java", +); +for (const marker of [ + "implements PhoneToolGateway", + "import android.openphone.OpenPhoneAgentManager", + "new FrameworkToolExecutor", + 'PROFILE = "openphone_os"', +]) { + if (!osGateway.includes(marker)) { + fail(`OpenPhone OS gateway is missing ${marker}`); + } +} + +const runtimeBridge = read( + "overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/runtime/RuntimeToolBridge.java", +); +for (const marker of [ + "mPhoneGateway.startTask", + "mPhoneGateway.executeTool", + "mPhoneGateway.confirmAction", + "mPhoneGateway.supportsTool", +]) { + if (!runtimeBridge.includes(marker)) { + fail(`RuntimeToolBridge does not route through ${marker}`); + } +} + +console.log("App/OS boundary contract checks passed.");