Repository navigation
Feature/eval framework #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
25a3ed6
369442a
d8cfb0f
1ed39d5
bb60177
cbc5556
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # LIVE evaluation suite — calls Sarvam for real and therefore costs money. | ||
| # | ||
| # Deliberately NOT on push/pull_request. The deterministic suite already gates | ||
| # every PR for free in android.yml; running paid model calls on every commit | ||
| # would cost more than it tells us. This is manual, plus a weekly drift check. | ||
| name: Evals (LIVE) | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| schedule: | ||
| # Weekly, Monday 03:00 UTC — catches model-side drift without a daily bill. | ||
| - cron: '0 3 * * 1' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| live-evals: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
|
Comment on lines
+8
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/evals-live.yml
printf '%s\n' '--- related workflow references ---'
rg -n --hidden -S 'evals-live|Sarvam|sarvam|concurrency:|workflow_dispatch:|schedule:' .github README.md 2>/dev/null || true
printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/*'Repository: NITISH-R-G/ScreenSaathi Length of output: 4684 🏁 Script executed: #!/bin/bash
set -eu
cat -n .github/workflows/evals-live.yml
rg -n --hidden -S 'evals-live|Sarvam|sarvam|concurrency:|workflow_dispatch:|schedule:' .github README.md 2>/dev/null || true
git ls-files '.github/workflows/*'Repository: NITISH-R-G/ScreenSaathi Length of output: 4608 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- existing concurrency configuration ---'
cat -n .github/workflows/android.yml | sed -n '1,25p'
printf '%s\n' '--- LIVE evaluation sources and tests ---'
rg -n --hidden -S 'SCREENSAATHI_EVAL_LIVE|SARVAM_API_KEY|eval-results|class .*Eval|`@Test`|Sarvam' app .github 2>/dev/null | head -250
printf '%s\n' '--- read-only workflow invariant check ---'
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/evals-live.yml")
text = p.read_text()
checks = {
"manual dispatch trigger": " workflow_dispatch:" in text,
"weekly schedule trigger": " schedule:" in text and "cron: '0 3 * * 1'" in text,
"LIVE mode enabled": "SCREENSAATHI_EVAL_LIVE: '1'" in text,
"Sarvam key passed": "SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }}" in text,
"no workflow concurrency key": "\nconcurrency:" not in text,
}
for name, result in checks.items():
print(f"{name}: {result}")
PYRepository: NITISH-R-G/ScreenSaathi Length of output: 24525 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- existing concurrency configuration ---'
cat -n .github/workflows/android.yml | sed -n '1,25p'
printf '%s\n' '--- LIVE evaluation sources and tests ---'
rg -n --hidden -S 'SCREENSAATHI_EVAL_LIVE|SARVAM_API_KEY|eval-results|class .*Eval|`@Test`|Sarvam' app .github 2>/dev/null | head -250
printf '%s\n' '--- read-only workflow invariant check ---'
python3 - <<'PY'
from pathlib import Path
text = Path(".github/workflows/evals-live.yml").read_text()
checks = {
"manual dispatch trigger": " workflow_dispatch:" in text,
"weekly schedule trigger": " schedule:" in text and "cron: '0 3 * * 1'" in text,
"LIVE mode enabled": "SCREENSAATHI_EVAL_LIVE: '1'" in text,
"Sarvam key passed": "SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }}" in text,
"no workflow concurrency key": "\nconcurrency:" not in text,
}
for name, result in checks.items():
print(f"{name}: {result}")
PYRepository: NITISH-R-G/ScreenSaathi Length of output: 24525 🌐 Web query:
💡 Result: When using a concurrency group in GitHub Actions with Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- LIVE test branches and request execution ---'
for f in \
app/src/test/java/com/screensaathi/evals/PlannerEvalTest.kt \
app/src/test/java/com/screensaathi/evals/LatencyProfileTest.kt \
app/src/test/java/com/screensaathi/evals/OpenEndedEvalTest.kt \
app/src/test/java/com/screensaathi/evals/DeviceAwarenessEvalTest.kt \
app/src/test/java/com/screensaathi/evals/EvalHarness.kt
do
echo "--- $f ---"
rg -n -C 5 'val live|SCREENSAATHI_EVAL_LIVE|newCall|execute\(|planOpenEnded|plan\(' "$f"
done
printf '%s\n' '--- read-only LIVE call-path summary ---'
python3 - <<'PY'
from pathlib import Path
import re
files = sorted(Path("app/src/test/java/com/screensaathi/evals").glob("*EvalTest.kt"))
for path in files:
text = path.read_text()
has_live = "SCREENSAATHI_EVAL_LIVE" in text
calls = len(re.findall(r'\.newCall\(', text))
executes = len(re.findall(r'\.execute\(', text))
print(f"{path}: live_guard={has_live}, newCall={calls}, execute={executes}")
PYRepository: NITISH-R-G/ScreenSaathi Length of output: 8101 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- LIVE test branches and request execution ---'
for f in \
app/src/test/java/com/screensaathi/evals/PlannerEvalTest.kt \
app/src/test/java/com/screensaathi/evals/LatencyProfileTest.kt \
app/src/test/java/com/screensaathi/evals/OpenEndedEvalTest.kt \
app/src/test/java/com/screensaathi/evals/DeviceAwarenessEvalTest.kt \
app/src/test/java/com/screensaathi/evals/EvalHarness.kt
do
echo "--- $f ---"
rg -n -C 5 'val live|SCREENSAATHI_EVAL_LIVE|newCall|execute\(|planOpenEnded|plan\(' "$f"
done
printf '%s\n' '--- read-only LIVE call-path summary ---'
python3 - <<'PY'
from pathlib import Path
import re
for path in sorted(Path("app/src/test/java/com/screensaathi/evals").glob("*EvalTest.kt")):
text = path.read_text()
print(
f"{path}: "
f"live_guard={'SCREENSAATHI_EVAL_LIVE' in text}, "
f"newCall={len(re.findall(r'\\.newCall\\(', text))}, "
f"execute={len(re.findall(r'\\.execute\\(', text))}"
)
PYRepository: NITISH-R-G/ScreenSaathi Length of output: 7897 Serialize LIVE evaluation runs. The manual and weekly triggers can overlap. LIVE mode makes paid Sarvam requests, so overlap can duplicate requests and increase rate-limit pressure. Add a workflow-level concurrency group with 🧰 Tools🪛 zizmor (1.29.0)[info] 18-18: workflow or action definition without a name (anonymous-definition): this job (anonymous-definition) [warning] 8-12: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting (concurrency-limits) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-java@v4 | ||
| with: | ||
| distribution: temurin | ||
| java-version: '21' | ||
|
|
||
| - uses: gradle/actions/setup-gradle@v4 | ||
| - uses: android-actions/setup-android@v3 | ||
|
|
||
| - name: Install SDK packages | ||
| run: sdkmanager --install "platforms;android-36.1" "build-tools;36.1.0" | ||
|
|
||
| # Skips rather than fails when the secret is absent, so a fork or a repo | ||
| # without the key does not show a permanently red scheduled job. | ||
| - name: Check for key | ||
| id: key | ||
| run: | | ||
| if [ -z "${{ secrets.SARVAM_API_KEY }}" ]; then | ||
| echo "::warning::SARVAM_API_KEY not set — skipping LIVE evals." | ||
| echo "present=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "present=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Run LIVE evals | ||
| if: steps.key.outputs.present == 'true' | ||
| env: | ||
| SCREENSAATHI_EVAL_LIVE: '1' | ||
| SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} | ||
| run: ./gradlew :app:testDebugUnitTest --tests "com.screensaathi.evals.*" --rerun-tasks --stacktrace | ||
|
|
||
| - name: Upload LIVE eval report | ||
| if: always() && steps.key.outputs.present == 'true' | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: eval-report-live | ||
| path: eval-results/ | ||
| if-no-files-found: ignore | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,22 +8,31 @@ | |
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> | ||
|
|
||
| <!-- Package visibility (Android 11+): without these, getLaunchIntentForPackage | ||
| returns null for every one of them and the app reports that no taxi app | ||
| is installed on a phone that plainly has three. Named explicitly rather | ||
| than QUERY_ALL_PACKAGES — we only ever offer this list. Keep in sync | ||
| with RideApps.KNOWN. --> | ||
| <!-- | ||
| Universal app discovery (Android 11+ package visibility). | ||
|
|
||
| This replaced a seven-package allow-list, which made the assistant | ||
| structurally incapable of its own core function: any app outside that list | ||
| was invisible, so "open WhatsApp" could only ever fail, and DeviceContext | ||
| correctly but uselessly reported UNKNOWN for nearly everything installed. | ||
|
|
||
| QUERY_ALL_PACKAGES is a Play-policy restricted permission. The | ||
| justification here is that app discovery IS the user-facing feature — the | ||
| user names an app and the assistant must determine whether it exists — and | ||
| a Play submission would have to declare exactly that. Nothing leaves the | ||
| device: the inventory is read on-device and only a compact, capped list of | ||
| labels ever reaches the model (see DeviceContext.toPromptText). | ||
| --> | ||
| <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> | ||
|
|
||
| <queries> | ||
| <package android:name="com.ubercab" /> | ||
| <package android:name="com.olacabs.customer" /> | ||
| <package android:name="com.rapido.passenger" /> | ||
| <package android:name="in.indrive.client" /> | ||
| <package android:name="com.blusmart.rider" /> | ||
| <package android:name="app.nammayatri.passenger" /> | ||
| <package android:name="com.meru.mgt" /> | ||
| <intent> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent> | ||
| </queries> | ||
|
|
||
| <application | ||
|
Check warning on line 35 in app/src/main/AndroidManifest.xml
|
||
| android:name=".SaathiApp" | ||
| android:allowBackup="false" | ||
| android:label="@string/app_name" | ||
|
|
@@ -42,6 +51,33 @@ | |
| </intent-filter> | ||
| </activity> | ||
|
|
||
| <!-- | ||
| ScreenSaathi as the home screen. | ||
|
|
||
| Opt-in by construction: declaring CATEGORY_HOME only makes the app | ||
| *eligible*; Android routes HOME here solely if the user selects | ||
| ScreenSaathi as their default launcher. A user who never does is | ||
| unaffected, and MainActivity remains the normal entry point. | ||
|
|
||
| stateUnchanged/adjustPan and the excludeFromRecents/singleTask combo | ||
| are the standard launcher posture — a home screen should not stack | ||
| instances or appear in Recents as if it were an ordinary app. | ||
| --> | ||
| <activity | ||
| android:name=".launcher.LauncherActivity" | ||
| android:exported="true" | ||
| android:launchMode="singleTask" | ||
| android:excludeFromRecents="true" | ||
| android:stateNotNeeded="true" | ||
| android:windowSoftInputMode="stateUnchanged|adjustPan" | ||
| android:theme="@style/Theme.ScreenSaathi"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.HOME" /> | ||
| <category android:name="android.intent.category.DEFAULT" /> | ||
| </intent-filter> | ||
| </activity> | ||
|
|
||
| <!-- The controlled screen the assistant guides the user through. Real | ||
| Android views with real resource ids, read live over accessibility. --> | ||
| <activity | ||
|
|
@@ -50,6 +86,21 @@ | |
| android:label="@string/demo_title" | ||
| android:windowSoftInputMode="adjustResize" /> | ||
|
|
||
| <!-- | ||
| Trigger a highlight without changing the foreground app. Exported so | ||
| it can be driven from adb during development and demos; it carries no | ||
| data and only starts our own already-running service. | ||
| --> | ||
| <receiver | ||
|
Check failure on line 94 in app/src/main/AndroidManifest.xml
|
||
| android:name=".HighlightReceiver" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
| <action android:name="com.screensaathi.HIGHLIGHT" /> | ||
| <action android:name="com.screensaathi.CLEAR_HIGHLIGHT" /> | ||
| <action android:name="com.screensaathi.DUMP_SCREEN" /> | ||
| </intent-filter> | ||
| </receiver> | ||
|
Comment on lines
+94
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Restrict the debug broadcast receiver.
Make this receiver non-exported in release builds. Put adb-only controls in the debug manifest, or require a signature-level permission. 🤖 Prompt for AI Agents |
||
|
|
||
| <service | ||
| android:name=".OverlayService" | ||
| android:exported="false" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package com.screensaathi | ||
|
|
||
| import android.content.BroadcastReceiver | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.util.Log | ||
|
|
||
| /** | ||
| * Triggers a highlight without disturbing whatever is on screen. | ||
| * | ||
| * An Activity-based trigger cannot work for this: starting an Activity brings | ||
| * it to the foreground, so by the time the resolver reads the accessibility | ||
| * tree the target app is no longer in front and it resolves against the wrong | ||
| * screen. A broadcast changes nothing about the foreground task. | ||
| * | ||
| * adb shell am broadcast -a com.screensaathi.HIGHLIGHT --es query "Wi-Fi" | ||
| * | ||
| * This is the same resolver and overlay the voice path uses — only speech | ||
| * recognition is skipped — so exercising it is a real test of the pipeline. | ||
| */ | ||
| class HighlightReceiver : BroadcastReceiver() { | ||
|
|
||
| override fun onReceive(context: Context, intent: Intent) { | ||
| when (intent.action) { | ||
| ACTION_HIGHLIGHT -> { | ||
| val query = intent.getStringExtra("query").orEmpty() | ||
| if (query.isBlank()) return | ||
| Log.d(TAG, "HIGHLIGHT_REQUEST query='$query'") | ||
| OverlayService.highlight(context, query) | ||
| } | ||
| // get_current_screen(): dump the live ScreenModel exactly as the | ||
| // resolver sees it. This is the tool the agent will call, and it is | ||
| // also the only honest way to tell "the node is missing from our | ||
| // snapshot" apart from "the node is there but scored too low". | ||
| ACTION_DUMP -> { | ||
| val snap = ScreenReaderService.instance?.snapshot() | ||
| if (snap == null) { | ||
| Log.w(TAG, "SCREEN_DUMP unavailable: accessibility service not bound") | ||
| return | ||
| } | ||
| Log.w(TAG, "SCREEN_DUMP pkg=${snap.packageName} settled=${snap.settled} " + | ||
| "elements=${snap.elements.size}") | ||
| val filter = intent.getStringExtra("filter")?.lowercase().orEmpty() | ||
| snap.elements.forEachIndexed { i, e -> | ||
| val line = "[$i] id='${e.resourceId}' text='${e.text}' cls=${e.className} " + | ||
| "click=${e.clickable} edit=${e.editable} bounds=${e.bounds.toShortString()}" | ||
| if (filter.isEmpty() || line.lowercase().contains(filter)) Log.w(TAG, " $line") | ||
| } | ||
| } | ||
|
Comment on lines
+35
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
fd -t f 'AndroidManifest.xml' | xargs -r rg -n -C6 'HighlightReceiver|DUMP_SCREEN|com.screensaathi.HIGHLIGHT|exported'Repository: NITISH-R-G/ScreenSaathi Length of output: 3171 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- HighlightReceiver.kt ---'
cat -n app/src/main/java/com/screensaathi/HighlightReceiver.kt
printf '%s\n' '--- manifest receiver context ---'
sed -n '80,108p' app/src/main/AndroidManifest.xml
printf '%s\n' '--- receiver call sites and action definitions ---'
rg -n -C3 'HighlightReceiver|ACTION_DUMP|DUMP_SCREEN|HIGHLIGHT|CLEAR_HIGHLIGHT|SCREEN_DUMP' app/src
printf '%s\n' '--- build configuration ---'
rg -n -C3 'buildTypes|BuildConfig|applicationId|namespace' app/build.gradle app/build.gradle.kts build.gradle build.gradle.kts settings.gradle settings.gradle.kts 2>/dev/null || trueRepository: NITISH-R-G/ScreenSaathi Length of output: 15543 🏁 Script executed: #!/bin/bash
python3 - <<'PY'
from pathlib import Path
import re
import xml.etree.ElementTree as ET
receiver = Path("app/src/main/java/com/screensaathi/HighlightReceiver.kt").read_text()
manifest = ET.parse("app/src/main/AndroidManifest.xml").getroot()
ns = {"a": "http://schemas.android.com/apk/res/android"}
branches = re.findall(r'\b(ACTION_[A-Z]+)\s*->\s*\{', receiver)
dump_block = receiver.split("ACTION_DUMP ->", 1)[1].split("ACTION_CLEAR ->", 1)[0]
logs_foreground_fields = all(
field in dump_block for field in ("snap.packageName", "e.resourceId", "e.text", "e.bounds")
)
debug_guarded = "BuildConfig.DEBUG" in dump_block
receiver_nodes = [
node for node in manifest.findall(".//receiver")
if node.get("{http://schemas.android.com/apk/res/android}name") == ".HighlightReceiver"
]
exported = receiver_nodes[0].get("{http://schemas.android.com/apk/res/android}exported") if receiver_nodes else None
actions = [
action.get("{http://schemas.android.com/apk/res/android}name")
for action in receiver_nodes[0].findall("./intent-filter/action", ns)
] if receiver_nodes else []
print({
"branches": branches,
"dump_logs_foreground_fields": logs_foreground_fields,
"dump_has_debug_guard": debug_guarded,
"receiver_exported": exported,
"receiver_actions": actions,
})
PYRepository: NITISH-R-G/ScreenSaathi Length of output: 444 Restrict The exported receiver logs foreground accessibility content at 🤖 Prompt for AI Agents |
||
| ACTION_CLEAR -> { | ||
| Log.d(TAG, "HIGHLIGHT_CLEAR") | ||
| OverlayService.clearHighlight(context) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| const val ACTION_HIGHLIGHT = "com.screensaathi.HIGHLIGHT" | ||
| const val ACTION_CLEAR = "com.screensaathi.CLEAR_HIGHLIGHT" | ||
| const val ACTION_DUMP = "com.screensaathi.DUMP_SCREEN" | ||
| private const val TAG = "HighlightReceiver" | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: NITISH-R-G/ScreenSaathi
Length of output: 15421
Pin the GitHub Actions to immutable commit SHAs.
Replace the mutable tags in the listed
android.ymlandevals-live.ymlsteps with full commit SHAs. These actions run in workflows with repository credentials and the LIVE evaluation secret.🧰 Tools
🪛 zizmor (1.29.0)
[error] 85-85: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
📍 Affects 2 files
.github/workflows/android.yml#L83-L89(this comment).github/workflows/evals-live.yml#L23-L31.github/workflows/evals-live.yml#L55-L61🤖 Prompt for AI Agents
Source: Linters/SAST tools