Phase 4: plugin manifest, permissions and event hooks
Browse files
app/src/main/java/io/github/abrarul/modularai/plugin/PluginHost.kt
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package io.github.abrarul.modularai.plugin
|
| 2 |
+
|
| 3 |
+
import android.content.Context
|
| 4 |
+
import io.github.abrarul.modularai.bridge.TermuxBridge
|
| 5 |
+
import org.json.JSONObject
|
| 6 |
+
import java.io.File
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Plugin layout: filesDir/plugins/<id>/plugin.json + entry script.
|
| 10 |
+
* plugin.json: {"id","name","version","entry":"run.sh","permissions":["net","shell"],"hooks":["on_capture","on_model_loaded"]}
|
| 11 |
+
* A plugin only runs hooks for permissions the user has explicitly granted.
|
| 12 |
+
*/
|
| 13 |
+
data class Plugin(
|
| 14 |
+
val id: String, val name: String, val version: String,
|
| 15 |
+
val entry: String, val permissions: Set<String>, val hooks: Set<String>, val dir: File
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
class PluginHost(private val ctx: Context) {
|
| 19 |
+
private val root = File(ctx.filesDir, "plugins").apply { mkdirs() }
|
| 20 |
+
private val granted = ctx.getSharedPreferences("plugin_grants", Context.MODE_PRIVATE)
|
| 21 |
+
|
| 22 |
+
fun installed(): List<Plugin> = root.listFiles()?.mapNotNull { load(it) } ?: emptyList()
|
| 23 |
+
|
| 24 |
+
private fun load(d: File): Plugin? = try {
|
| 25 |
+
val j = JSONObject(File(d, "plugin.json").readText())
|
| 26 |
+
fun set(k: String) = j.optJSONArray(k)?.let { a -> (0 until a.length()).map { a.getString(it) }.toSet() } ?: emptySet()
|
| 27 |
+
Plugin(j.getString("id"), j.getString("name"), j.getString("version"),
|
| 28 |
+
j.getString("entry"), set("permissions"), set("hooks"), d)
|
| 29 |
+
} catch (_: Exception) { null }
|
| 30 |
+
|
| 31 |
+
fun grant(id: String, perms: Set<String>) = granted.edit().putStringSet(id, perms).apply()
|
| 32 |
+
fun isGranted(p: Plugin) = granted.getStringSet(p.id, emptySet())!!.containsAll(p.permissions)
|
| 33 |
+
fun remove(p: Plugin) { p.dir.deleteRecursively(); granted.edit().remove(p.id).apply() }
|
| 34 |
+
|
| 35 |
+
/** Fires [event]; payload is passed as the first script argument. */
|
| 36 |
+
fun emit(event: String, payload: String = "") {
|
| 37 |
+
installed().filter { event in it.hooks && isGranted(it) }.forEach { p ->
|
| 38 |
+
// Entry paths must stay inside the plugin dir.
|
| 39 |
+
val entry = File(p.dir, p.entry).canonicalFile
|
| 40 |
+
if (!entry.path.startsWith(p.dir.canonicalPath)) return@forEach
|
| 41 |
+
if ("shell" in p.permissions) {
|
| 42 |
+
TermuxBridge.run(ctx, "sh '${entry.path}' '$event' \"\$1\"", arrayOf("_", payload))
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|