@@ -31,18 +31,41 @@ class ClawRunner(private val context: Context) {
3131 private val configFile get() = File (filesDir, CONFIG_NAME )
3232 private val nullclawConfigFile get() = File (homeDir, " .nullclaw/config.json" )
3333
34+ private val caBundleFile get() = File (filesDir, " ca-certificates.crt" )
35+
3436 /* * Write NullClaw home config with API key and model, copy app config from assets. */
3537 suspend fun ensureInstalled () = withContext(Dispatchers .IO ) {
3638 Log .i(TAG , " NullClaw binary at ${binaryFile.absolutePath} (exists: ${binaryFile.exists()} )" )
37- // Only copy app config if it doesn't exist
3839 if (! configFile.exists()) {
3940 context.assets.open(CONFIG_NAME ).use { it.copyTo(configFile.outputStream()) }
4041 }
41- // Always write ~/.nullclaw/config.json with API key + model so NullClaw is configured
4242 writeNullclawHomeConfig()
43+ buildCaBundle()
4344 Log .i(TAG , " NullClaw ready, home=${homeDir.absolutePath} " )
4445 }
4546
47+ /* * Bundle Android system CA certs into a single PEM file for Zig's TLS. */
48+ private fun buildCaBundle () {
49+ if (caBundleFile.exists()) return
50+ val certDirs = listOf (
51+ " /apex/com.android.conscrypt/cacerts" ,
52+ " /system/etc/security/cacerts"
53+ )
54+ val bundle = StringBuilder ()
55+ for (dir in certDirs) {
56+ val d = File (dir)
57+ if (! d.exists()) continue
58+ d.listFiles()?.forEach { cert ->
59+ try { bundle.append(cert.readText()).append(" \n " ) } catch (_: Exception ) {}
60+ }
61+ if (bundle.isNotEmpty()) break
62+ }
63+ if (bundle.isNotEmpty()) {
64+ caBundleFile.writeText(bundle.toString())
65+ Log .i(TAG , " CA bundle written: ${caBundleFile.absolutePath} " )
66+ }
67+ }
68+
4669 /* * Write ~/.nullclaw/config.json so NullClaw knows the model + provider. */
4770 private fun writeNullclawHomeConfig () {
4871 val apiKey = getApiKey() ? : return
@@ -52,7 +75,7 @@ class ClawRunner(private val context: Context) {
5275 "agents": {
5376 "defaults": {
5477 "model": {
55- "primary": "claude-opus-4-6"
78+ "primary": "anthropic/ claude-opus-4-6"
5679 }
5780 }
5881 },
@@ -100,11 +123,19 @@ class ClawRunner(private val context: Context) {
100123 put(" ANTHROPIC_API_KEY" , apiKey)
101124 put(" HOME" , homeDir.absolutePath)
102125 put(" PATH" , " /system/bin:/system/xbin" )
126+ // CA bundle for Zig's TLS (musl has no system store on Android)
127+ if (caBundleFile.exists()) {
128+ put(" SSL_CERT_FILE" , caBundleFile.absolutePath)
129+ put(" CURL_CA_BUNDLE" , caBundleFile.absolutePath)
130+ put(" CAINFO" , caBundleFile.absolutePath)
131+ }
103132 }
104133 }
105134 .start()
106135
107136 // Fix #7: read stdout and stderr concurrently
137+ Log .i(TAG , " NullClaw running: prompt='${prompt.take(50 )} ' binary=${binaryFile.absolutePath} home=${homeDir.absolutePath} config=${nullclawConfigFile.exists()} " )
138+
108139 var output = " "
109140 var error = " "
110141 val stdoutThread = Thread { output = process.inputStream.bufferedReader().readText() }
@@ -115,9 +146,14 @@ class ClawRunner(private val context: Context) {
115146 stdoutThread.join()
116147 stderrThread.join()
117148
149+ Log .i(TAG , " NullClaw exit=$exit output='${output.take(100 )} ' stderr='${error.take(200 )} '" )
150+
118151 if (exit != 0 ) {
119- Log .e(TAG , " NullClaw exit $exit : $error " )
152+ Log .e(TAG , " NullClaw failed exit $exit : $error " )
120153 Result .failure(RuntimeException (error.take(120 )))
154+ } else if (output.isBlank()) {
155+ Log .e(TAG , " NullClaw returned empty output. stderr: $error " )
156+ Result .failure(RuntimeException (if (error.isNotBlank()) error.take(120 ) else " Empty response" ))
121157 } else {
122158 Result .success(output.trim())
123159 }
0 commit comments