Skip to content
Merged
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
13 changes: 7 additions & 6 deletions bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ so `bin/agent` provisions nothing and sends nothing. It names the script; the ca

Every screen the wizard shows has a twin in `next.question`, including registering a missing
Android app and picking the device by name, so the chat is the whole walk rather than a router to
the terminal. `run_app` keeps its question even after the device is settled: signing in and
answering Android's notification dialog happen inside the developer's app, so the tool asks and
waits.
the terminal. `run_app` keeps its question even after the device is settled: `bin/launch-app` opens
the selected package, then signing in and answering Android's notification dialog happen inside
the developer's app, so the tool asks and waits.

**Nothing in here ever works the developer's device.** The gates read `dumpsys` and `logcat` and
never tap, type or launch — a guessed tap is indistinguishable from a slow screen, and a permission
the tool granted itself is not evidence of anything.
The gates still only read `dumpsys` and `logcat`. The one device actor, `bin/launch-app`, starts the
selected package's launcher activity and stops there: it never taps, types, signs in, or grants a
permission. A permission the tool granted itself would not be evidence of anything.

### Who drives the Google setup stays open until the developer answers

Expand Down Expand Up @@ -131,6 +131,7 @@ The split is structural rather than conventional.
| `bin/agent` | **Reporter.** Runs the verifier and serialises its state; provisions nothing, sends nothing |
| `bin/provision` | **Actor.** Idempotent; ends by handing off to `bin/gates` to be judged |
| `bin/proof-push` | **Actor.** Sends the one push the ladder exists to prove, and records the marker it sent. Never checks whether it arrived |
| `bin/launch-app` | **Actor.** Opens the selected package's launcher activity on the selected device. Never taps, types, signs in, or grants permission |
| `bin/discover` | Read-only list of the developer's Firebase projects and their Android apps |
| `bin/iterable-keys` | Walks the four Iterable dashboard steps that have no API, captures the keys, then hands off to `bin/gates`. Offers the identity the app already registered, read from the device |
| `bin/handoff` | Prints the block that sends a developer to their own terminal, for an agent to relay verbatim. Changes nothing |
Expand Down
2 changes: 1 addition & 1 deletion bin/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ws_init() {

# The entry points a developer is ever told to run. The rest of bin/ is internal —
# wizard and provision are dispatched to, never typed.
SHIM_CMDS="onboard teardown discover gates agent iterable-keys proof-push"
SHIM_CMDS="onboard teardown discover gates agent iterable-keys proof-push launch-app"

# A host installs a plugin into a version-numbered cache directory, which is both too
# long to type and not stable: old version directories stay on disk, so a path a
Expand Down
38 changes: 38 additions & 0 deletions bin/launch-app
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Opens the selected app's launcher activity on the selected Android device.
#
# This is deliberately the whole extent of the device automation: no taps, text,
# key events, sign-in, or permission grants. The person still answers everything
# inside their app; this actor only saves them from finding and opening it after
# an install.

source "$(dirname "${BASH_SOURCE[0]}")/config.sh"

[[ -n "$PACKAGE" ]] || {
echo "no package selected yet — choose the Android app before trying to launch it" >&2
exit 10
}

d="$(device_serial)" || {
echo "$d" >&2
exit 10
}

adb -s "$d" shell pm path "$PACKAGE" 2>/dev/null | grep -q '^package:' || {
echo "$PACKAGE is not installed on $d — build and install it before trying to launch it" >&2
exit 20
}

out="$(adb -s "$d" shell am start -W \
-a android.intent.action.MAIN \
-c android.intent.category.LAUNCHER \
-p "$PACKAGE" 2>&1)"
rc=$?

if ((rc != 0)) || grep -qiE '(^|[[:space:]])(error|exception):|unable to resolve|does not exist' <<< "$out"; then
echo "could not open $PACKAGE on $d:" >&2
printf '%s\n' "$out" >&2
exit 20
fi

echo "Opened $PACKAGE on $d."
2 changes: 1 addition & 1 deletion iterable-provision/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ relay the block, do not look for a flag that gets you past it, and never set
| `project_unreachable` | Relay `next.summary` verbatim — it is a permissions answer from Google, not something to work around — and then ask `next.question`, which offers aiming somewhere else. A typo'd project id and a project they have no access to look identical from here, and both are answered at that screen. |
| `register_app` | Their project has no Android app for this package. Ask `next.question` — this is the one change the limits promise never happens unless they ask outright, so its yes is separate from the provisioning yes and recorded with `<root>/bin/agent approve create-app`. Never `CREATE_APP=1`. |
| `iterable_keys` | Four steps in their Iterable account that only they can do. Ask `next.question`, then walk them **one at a time** — see *The four Iterable steps* below. The two keys reach the workspace with `iterable-keys --take server\|mobile`, off their clipboard; never into this conversation. |
| `install_app` / `run_app` | The device half — hand off to `iterable-verify`. Until a device is on record `next.question` is the device picker, worth asking even when only one is attached: a phone left plugged in to charge is "the only device", takes the proof push, and the developer watching an emulator reports that nothing arrived. After that, `run_app` keeps a question of its own — signing in and allowing notifications happen inside their app, by their hand. Relay it. Never drive their device over adb. |
| `install_app` / `run_app` | The device half — hand off to `iterable-verify`. Until a device is on record `next.question` is the device picker, worth asking even when only one is attached: a phone left plugged in to charge is "the only device", takes the proof push, and the developer watching an emulator reports that nothing arrived. The verifier may open the selected app after installation; signing in and allowing notifications still happen inside their app, by their hand. |
| `send_proof` | The device half — hand off to `iterable-verify`. |
| `done` | A real push reached the device. Say which device and chain onward. |

Expand Down
32 changes: 16 additions & 16 deletions iterable-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ description: >-
Use when a push is not arriving, when a developer wants proof the integration
works, or right after `iterable-android` / `iterable-react-native` writes the
integration — including "why isn't my push arriving?", "is the token registered?",
"prove this works", "test push notifications". Reads only; it never provisions,
never edits the app, and never re-sends to manufacture a pass.
"prove this works", "test push notifications". Its gates read only; it never
provisions, edits the app, or re-sends to manufacture a pass. After installation,
it may open the selected app while leaving every in-app action to the developer.
---

# Proving Iterable push on a device
Expand All @@ -16,19 +17,18 @@ Eighteen checks, of which the last five are the device half. One of them is the
that matters — *push arrives on device*: an Iterable push, sent on purpose and
carrying a marker, found in the device's own notification records.

**This skill writes nothing and sends nothing on its own.** The verifier does not
trigger the thing it verifies — a checker that sends its own push can only ever
agree with itself. Sending is `<root>/bin/proof-push`, run as a separate, announced step.
**The verification gates write nothing and send nothing.** They do not trigger the
thing they verify — a checker that sends its own push can only ever agree with itself.
Sending is `<root>/bin/proof-push`, run as a separate, announced step.

**And it reads the device without operating it.** `adb shell dumpsys`, `logcat`, `pm
path` — yes. `adb shell input` (tap, text, keyevent), `am start`, `monkey`, `pm grant`,
or any other way of working their app for them — no, at any point, including when it
would be quicker. Three reasons, and each is sufficient: you cannot see their login
screen, so every field is a guess and a wrong guess looks exactly like a slow screen;
it costs minutes per attempt and often does not work; and the notification permission
is a question Android asks *the person*, so a tool that taps Allow has answered it on
their behalf and then reports the answer as evidence. When the device needs a human,
the tool says so in a question — relay it and wait.
**The gates read the device without operating it.** `adb shell dumpsys`, `logcat`, `pm
path` — yes. One separate actor, `<root>/bin/launch-app`, may start the selected
package's launcher activity after installation. That is the boundary: `adb shell
input` (tap, text, keyevent), `monkey`, `pm grant`, or any other way of working their
app for them — no, at any point. You cannot see their login screen, so every field is
a guess; and the notification permission is a question Android asks *the person*, so
a tool that taps Allow has answered it on their behalf and then reports the answer as
evidence. Open the app, relay the human question, and wait.

## Finding the scripts

Expand Down Expand Up @@ -81,8 +81,8 @@ that is still wrong.

| `next.kind` | What you do |
|---|---|
| `install_app` | Their app is not on the device, or the installed APK does not carry the SDK. Build and install, then re-run. The check reads `dumpsys package` for the Iterable messaging service, so it catches "integrated the SDK, forgot to reinstall" — invisible to anything that only reads the repo. |
| `run_app` | Their hands, not yours. Relay `next.question` — it asks them to sign in as `target.email` and allow notifications, and one of its options re-runs the checks. Do not launch the app, drive the UI, or answer the permission dialog (see below). |
| `install_app` | Their app is not on the device, or the installed APK does not carry the SDK. Build and install it, run `<root>/bin/launch-app`, then re-run the agent. The check reads `dumpsys package` for the Iterable messaging service, so it catches "integrated the SDK, forgot to reinstall" — invisible to anything that only reads the repo. |
| `run_app` | Run `<root>/bin/launch-app` first, then relay `next.question`. It asks them to sign in as `target.email` and allow notifications, and one option re-runs the checks. Opening the launcher activity is yours; driving the UI or answering the permission dialog is never yours. |
| `send_proof` | Announce it, then run `<root>/bin/proof-push`. It sends one marked push and deliberately does not check whether it arrived. Then re-run `<root>/bin/agent`. |
| `campaign_send` | Server-side corroboration needs `ITBL_CAMPAIGN_ID`; without it there is nothing to read. Optional. |
| `iterable_keys`, `provision`, `choose_target`, … | Not this skill's half — hand back to `iterable-provision`. |
Expand Down
108 changes: 108 additions & 0 deletions tests/launch-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
# Pins the one allowed device action: open the chosen package's launcher activity.
# Everything after that — sign-in and the notification prompt — remains human work.

set -uo pipefail
cd "$(dirname "$0")/.."

FAILED=0
ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAILED=1; }

STUB="$(mktemp -d)"
trap 'rm -rf "$STUB"' EXIT
mkdir -p "$STUB/ws"

cat > "$STUB/adb" <<'ADB'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$STUB_CALLS"

if [[ "$1" == devices ]]; then
printf 'List of devices attached\nemulator-5554\tdevice\n'
exit 0
fi

[[ "$1 $2" == "-s emulator-5554" ]] || exit 1
shift 2
case "$*" in
"emu avd name") echo Pixel_9_Pro ;;
"shell pm path com.example.app")
[[ "${STUB_INSTALLED:-1}" == 1 ]] && echo package:/data/app/com.example.app/base.apk
;;
"shell am start -W -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -p com.example.app")
if [[ "${STUB_LAUNCHABLE:-1}" == 1 ]]; then
printf 'Starting: Intent\nStatus: ok\n'
else
echo 'Error: Activity not started, unable to resolve Intent' >&2
exit 1
fi
;;
esac
ADB
chmod +x "$STUB/adb"

run_launch() {
PATH="$STUB:$PATH" WS="$STUB/ws" PACKAGE="${PACKAGE_UNDER_TEST-com.example.app}" \
TARGET_DEVICE=Pixel_9_Pro STUB_CALLS="$STUB/calls" \
STUB_INSTALLED="${STUB_INSTALLED:-1}" STUB_LAUNCHABLE="${STUB_LAUNCHABLE:-1}" \
bin/launch-app 2>&1
}

echo
echo " Opening the installed app"
echo

: > "$STUB/calls"
out="$(run_launch)"; rc=$?
if ((rc == 0)) && [[ "$out" == *"Opened com.example.app on emulator-5554."* ]]; then
ok "opens the selected package on the selected device"
else
bad "expected a successful launch, got rc $rc: $out"
fi

expected="shell am start -W -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -p com.example.app"
grep -qF "$expected" "$STUB/calls" \
&& ok "uses the package's MAIN/LAUNCHER activity" \
|| bad "did not issue the expected launch intent"

if grep -qE 'shell (input|monkey)|pm grant|keyevent| tap | text ' "$STUB/calls"; then
bad "used device input or granted a permission"
else
ok "does not tap, type, sign in, or grant permission"
fi

echo
echo " Refusing states that cannot be launched"
echo

: > "$STUB/calls"
STUB_INSTALLED=0 out="$(run_launch)"; rc=$?
if ((rc == 20)) && [[ "$out" == *"is not installed"* ]] \
&& ! grep -q 'shell am start' "$STUB/calls"; then
ok "does not try to open an app that is not installed"
else
bad "uninstalled app should stop before launch, got rc $rc: $out"
fi

: > "$STUB/calls"
PACKAGE_UNDER_TEST="" out="$(run_launch)"; rc=$?
if ((rc == 10)) && [[ "$out" == *"no package selected"* ]] \
&& ! grep -q 'shell am start' "$STUB/calls"; then
ok "does not guess which package to open"
else
bad "missing package should ask for a choice, got rc $rc: $out"
fi

: > "$STUB/calls"
unset STUB_INSTALLED
unset PACKAGE_UNDER_TEST
STUB_LAUNCHABLE=0 out="$(run_launch)"; rc=$?
if ((rc == 20)) && [[ "$out" == *"could not open"* ]]; then
ok "reports a package with no launchable activity"
else
bad "unlaunchable package should fail clearly, got rc $rc: $out"
fi

echo
((FAILED)) && { echo " FAILED"; exit 1; }
echo " All good — launch stops where human interaction begins."
2 changes: 1 addition & 1 deletion tests/no-bare-commands.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ SW="$(mktemp -d)/proj"; mkdir -p "$SW"
( cd "$SW" && WS="$SW/.iterable" bash -c "source '$ROOT/bin/config.sh'; ws_init" ) >/dev/null 2>&1

missing=""
for c in onboard teardown discover gates agent iterable-keys proof-push; do
for c in onboard teardown discover gates agent iterable-keys proof-push launch-app; do
[[ -x "$SW/.iterable/$c" ]] || missing="$missing $c"
done
[[ -z "$missing" ]] && ok "every entry point a developer is told to run gets one" \
Expand Down
Loading