Framework provides primitives; helpers are not framework¶
Summary¶
The framework’s built-in tool surface is low-level primitives only — the smallest, most general operations a driver/executor can perform, each with no more configuration than the operation inherently needs. Ergonomic, task-shaped helpers compose those primitives, but they are not framework tools: they live in the consuming trailmap, or as an inline composition at the call site.
This was hammered out reviewing a “write a file to Downloads” tool. The honest endpoint surprised the starting point, so the reasoning is worth recording.
The decision¶
-
The framework ships primitives. A primitive is the lowest-level operation the executor can do, and it carries only the config that operation inherently needs. If a candidate tool wants a pile of optional knobs (owner, mode, MIME, relative path, target collection…), that’s the tell it’s a composition, not a primitive — don’t ship it as a structured tool.
-
The general primitive already exists:
android_adbShell. It’s argv-complete, so the entire long tail is expressible today with zero new surface:chmod/chown/restorecon,content insert --bind …,cmd media_scanner scan,base64 -d > path. It never needs another param. -
Helpers are not framework. A convenience that only adds a transform on top of a primitive (e.g. “UTF-8-encode this string, then write the bytes”) carries no framework-general value. It belongs to whoever needs it — inline, or a tool in their own trailmap. A helper in the framework is a smell, not a feature.
What ships¶
Exactly one new framework tool:
android_writeBytesToFile(devicePath, base64Content)— writes raw bytes to an absolute device path (creating parents, overwriting). Backed by the new dual-modeAndroidDeviceCommandExecutor.writeFileToDevice: host =adb push(sync protocol); on-device = a directjava.io.Filewrite, falling back to a temp-file +cpfor paths that need theshellUID.
That’s it. The original motivating shape — “write this setup JSON to Downloads”
— is a composition over this primitive (and, if the consumer needs the file
MediaStore-visible, an android_adbShell cmd media_scanner scan), owned by the
consuming app, not the framework.
Why this one primitive earns a slot (and nothing else does)¶
android_adbShell already covers writing bytes and everything around it —
except one thing: it can’t move a file body reliably.
- On the host, piping a body through
adb shellstdin hits the EXIT-packet hang the executor already documents (it usesadb pushto avoid it). - On-device, passing a base64 blob as a shell argument hits
ARG_MAXfor large payloads.
So “get a (possibly large/binary) body onto the device” is the single capability
adbShell lacks — and that’s about a path, not a directory or a provider.
android_writeBytesToFile does exactly that and nothing more: path + bytes, no
MIME, no MediaStore, no perms. Those stay on adbShell, which keeps the primitive
free of param sprawl.
Key decisions (and the dead ends that got us here)¶
-
Bytes, not text. Text is one encoding of bytes; tool args are a text-only wire, so bytes ride as base64. base64 carries no metadata — no MIME, no name (a
data:URI puts MIME in a prefix outside the base64). And a filesystem path write has no MIME concept anyway, so the primitive needs no type config — the MIME question simply doesn’t arise here. -
Filesystem path, not “Downloads”. An earlier cut was Downloads-specific and MediaStore-backed. Two problems killed it: (1) it needed config to be honest — at minimum a
mimeType(the executor hardcodestext/json, actively wrong for a binary payload) and a subdirectory — i.e. the param sprawl rule 1 forbids; (2) its only solid framework justification (the body-transfer hang) is about a path, not Downloads. MediaStore registration is separable and lives onadbShell. So the primitive is path-shaped; Downloads/MediaStore/MIME are caller compositions. -
Dead end: one tool with a
base64: Booleanflag. Conflated text and binary on one surface and hid the layering. Removed. -
Dead end: a UTF-8-text helper as a framework tool (delegating to the byte tool). Cleaner than the flag, but still a helper in the framework — exactly what rule 3 forbids. The text convenience is
base64(utf8(x))+ call; that’s caller-side. Removed. -
Dead end: a Downloads-specific byte tool (
android_writeBytesToDownloads). See “Filesystem path, not Downloads” above. Replaced by the path primitive.
Guidance for future framework tools¶
When adding a built-in, ask: what is the most general operation the executor performs, and how many knobs does it want?
- Wants many knobs (perms, owner, MIME, provider columns, arbitrary collections)
→ it’s
android_adbShell, not a structured tool. Don’t build it. - A fixed-scope operation a general shell genuinely can’t do (here: a hang-free, ARG_MAX-free body transfer) → ship a focused primitive with a minimal, stable signature.
- “A primitive plus a convenience step” → ship the primitive; the convenience is the caller’s (inline, or their trailmap).
Conventions unchanged: framework primitives keep flat / android_* / mobile_*
names (per trailmap-scoped tool naming),
surfaceToLlm = false for composition primitives, dual-mode (requiresHost
defaulted false) when the executor method has both host and on-device actuals.
Future work / watch items¶
- Writing to public Downloads MediaStore-visibly is now a composition:
android_writeBytesToFileto the Download path, then (only if the consumer reads via a MediaStore query rather than the filesystem path) anandroid_adbShellcmd media_scanner scan. The executor’s MediaStore-insertwriteFileToDownloadsstill exists for Kotlin API callers that need an atomic registered write; it just isn’t exposed as a tool. - UTF-8 → base64 in the scripted environment can be a footgun (
btoaisn’t UTF-8-safe in QuickJS). If a consuming app’s launch migration hits it, the answer is a helper in that trailmap, not a framework tool.