{"slug": "when-send-isnt-risky-context-aware-confirmation-in-an-android-ai-agent", "title": "When “Send” Isn’t Risky: Context-Aware Confirmation in an Android AI Agent", "summary": "A developer extended an Android AI agent's confirmation guard to weigh the foreground app's package name alongside the action label, so that a \"send\" in Gmail or PayPal triggers a confirmation prompt while the same word in a notes app does not. The design classifies packages into high, medium and low risk tiers and applies different keyword sets to each, using whole-word matching to avoid false positives like \"payload\" or \"sender\". The developer argues that over-triggering confirmations trains users to tap through prompts, undermining the guard's real protection.", "body_md": "In the last post I covered the two-layer confirmation guard: the LLM's `requires_confirmation` flag plus a keyword guard as a dumb-but-reliable backstop. A reader raised the right follow-up: \"send\" in a banking app and \"send\" in a notes app are not the same risk. The guard shouldn't treat them the same way.\n\nThis post covers the next layer — making the confirmation guard context-aware by weighing app identity and action type together.\n\nA flat list of risky keywords has two failure modes.\n\n**False negatives** (the dangerous kind): the guard misses a risky action because the label is in another language, uses a synonym, or comes from a resource ID the guard never sees.\n\n**False positives** (the usability kind): the guard fires on \"payload\", \"sender\", \"shareholder\" — or on \"send\" in an app where sending is so routine it carries no real risk. If confirmations fire too often on benign actions, users train themselves to tap through without reading. When the real confirmation arrives, it gets tapped through too.\n\nThe second failure mode undermines the first. A guard that cries wolf enough times stops being a guard.\n\nThe `AgentStepRequest` already carries `current_package` — the package name of the app currently in focus:\n\n```\nclass AgentStepRequest(BaseModel):\n    task: str\n    screen_description: str\n    current_package: Optional[str] = None  // e.g. \"com.google.android.gm\"\n    ...\n```\n\nThis is the signal that makes context-aware risk assessment possible. The same action means different things in different apps.\n\n```\nenum class AppRiskTier {\n    HIGH,    // Banking, payments, email clients, messaging\n    MEDIUM,  // Social, productivity, calendar\n    LOW      // Notes, utilities, entertainment\n}\n\nobject AppRiskClassifier {\n\n    private val HIGH_RISK_PACKAGES = setOf(\n        \"com.google.android.gm\",           // Gmail\n        \"com.android.mms\",                  // SMS\n        \"org.telegram.messenger\",           // Telegram\n        \"com.whatsapp\",                     // WhatsApp\n        \"com.paypal.android.p2pmobile\",    // PayPal\n        \"com.google.android.apps.nbu.paisa.user\" // GPay\n    )\n\n    private val LOW_RISK_PACKAGES = setOf(\n        \"com.google.android.keep\",         // Keep Notes\n        \"com.spotify.music\",               // Spotify\n        \"com.google.android.youtube\"       // YouTube\n    )\n\n    fun tierFor(packageName: String?): AppRiskTier {\n        if (packageName == null) return AppRiskTier.MEDIUM\n        return when {\n            HIGH_RISK_PACKAGES.any { packageName.startsWith(it) } -> AppRiskTier.HIGH\n            LOW_RISK_PACKAGES.any { packageName.startsWith(it) } -> AppRiskTier.LOW\n            else -> AppRiskTier.MEDIUM\n        }\n    }\n}\nobject ConfirmationGuard {\n\n    private val HIGH_RISK_HINTS = setOf(\n        \"send\", \"pay\", \"transfer\", \"purchase\", \"buy\",\n        \"confirm payment\", \"place order\", \"checkout\"\n    )\n\n    private val MEDIUM_RISK_HINTS = setOf(\n        \"delete\", \"remove\", \"uninstall\", \"submit\",\n        \"sign out\", \"log out\", \"deactivate\",\n        \"cancel subscription\", \"share\"\n    )\n\n    private val LOW_RISK_HINTS = setOf(\n        \"post\", \"publish\", \"save\"\n    )\n\n    fun shouldConfirm(\n        text: String?,\n        packageName: String?\n    ): Boolean {\n        if (text == null) return false\n        val lower = text.lowercase().trim()\n        val tier = AppRiskClassifier.tierFor(packageName)\n\n        return when (tier) {\n            AppRiskTier.HIGH ->\n                HIGH_RISK_HINTS.any { matchesWord(lower, it) } ||\n                MEDIUM_RISK_HINTS.any { matchesWord(lower, it) }\n\n            AppRiskTier.MEDIUM ->\n                HIGH_RISK_HINTS.any { matchesWord(lower, it) } ||\n                MEDIUM_RISK_HINTS.any { matchesWord(lower, it) }\n\n            AppRiskTier.LOW ->\n                HIGH_RISK_HINTS.any { matchesWord(lower, it) }\n        }\n    }\n\n    // Whole-word match to avoid \"pay\" in \"payload\", \"send\" in \"sender\"\n    private fun matchesWord(text: String, keyword: String): Boolean {\n        val pattern = Regex(\"\\\\b${Regex.escape(keyword)}\\\\b\")\n        return pattern.containsMatchIn(text)\n    }\n}\n```\n\nTwo changes from the flat version: whole-word matching instead of substring matching, and threshold variation by app tier. \"Send\" in a notes app (LOW tier) doesn't trigger confirmation. \"Send\" in Gmail (HIGH tier) does.\n\nA reader flagged the language gap in the last post — a phone set to French or Arabic renders \"Envoyer\" or \"إرسال\", neither of which matches \"send\". The visible label isn't reliable across locales.\n\nThe fix: use the view's `contentDescription` or resource ID alongside the label. Resource IDs are set by the developer and almost always stay in English regardless of locale:\n\n```\nfun shouldConfirm(\n    visibleText: String?,\n    contentDescription: String?,\n    resourceId: String?,\n    packageName: String?\n): Boolean {\n    // Check visible label, contentDescription, and resource ID\n    // Resource IDs travel across locales; visible text does not\n    return listOf(visibleText, contentDescription, resourceId)\n        .any { text ->\n            shouldConfirmText(text, packageName)\n        }\n}\n```\n\nThe Accessibility API surfaces all three. Using all three means the guard fires correctly regardless of the device language.\n\n```\nenum class ConfirmationSource {\n    LLM_FLAGGED,\n    KEYWORD_GUARD,\n    CONTEXT_AWARE_GUARD,\n    BOTH\n}\n```\n\n`CONTEXT_AWARE_GUARD` in the log means the flat guard would have missed it (LOW tier, medium-risk keyword) or the flat guard would have over-fired (LOW tier, low-risk keyword suppressed). Both are worth tracking separately — one shows where context is adding value, the other shows where it's reducing noise.\n\nA confirmation guard that fires on everything trains users to ignore it. A guard that knows which app it's running in fires less often and means more when it does. Context doesn't weaken the protection — it concentrates it where the risk is real.\n\n**Series so far:**", "url": "https://wpnews.pro/news/when-send-isnt-risky-context-aware-confirmation-in-an-android-ai-agent", "canonical_source": "https://dev.to/starebrain/when-send-isnt-risky-context-aware-confirmation-in-an-android-ai-agent-m5n", "published_at": "2026-10-29 03:06:16+00:00", "updated_at": "2026-10-29 03:11:23.530436+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "artificial-intelligence"], "entities": ["Gmail", "WhatsApp", "Telegram", "PayPal", "Google Pay", "Google Keep", "Spotify", "YouTube"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/when-send-isnt-risky-context-aware-confirmation-in-an-android-ai-agent", "markdown": "https://wpnews.pro/news/when-send-isnt-risky-context-aware-confirmation-in-an-android-ai-agent.md", "text": "https://wpnews.pro/news/when-send-isnt-risky-context-aware-confirmation-in-an-android-ai-agent.txt", "jsonld": "https://wpnews.pro/news/when-send-isnt-risky-context-aware-confirmation-in-an-android-ai-agent.jsonld"}}