Abrarul8541 commited on
Commit
9e996fb
·
verified ·
1 Parent(s): fe64f73

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
+ }