Skip to content

Commit c786bb2

Browse files
neurophone follow-up: JNI surface (#110), proofs, RSR, Trustfile, docs (#156)
## Summary Follow-up to the merged #154 (which landed the rand build-fix + CI re-pin). Continues driving `neurophone` toward fully-proven / fully-engineered: issue disposition, QA + security, proofs, RSR compliance, the estate Trustfile, and docs. **Draft — actively in progress**; this body tracks live state. ## Landed so far - **ci: remove retired `scorecard-enforcer.yml`** — the standards governance staleness gate (`check-workflow-staleness.sh`) hard-errors on this legacy file and on its per-push-only SARIF upload to Code Scanning. The estate retired it in favour of `scorecard.yml` → standards `scorecard-reusable.yml`. Deleting it is how the running gate is satisfied (solutions at source). Verified: staleness check passes locally. - **feat(#110): real NativeLib JNI surface** — the 6-line stub in `crates/neurophone-android` becomes the full 11-method `ai.neurophone.NativeLib` boundary from `docs/migrations/JNI-SURFACE-AUDIT.adoc`. jni 0.22 safe `EnvUnowned::with_env` (no unsafe *code*; panics caught at the boundary). Bridge logic is JVM-free and host-tested; `reset` consumes the core `shutdown(self)` typestate (Active→Down, once), wiring the resource-lifecycle property. Adds `QueryRoute` + `query_routed` + `get_neural_context` to core (non-breaking; `query` delegates). Gossamer-independent. Ground truth: 155 tests, clippy `-D warnings` clean, fmt clean. ## In progress - [ ] Proofs (#84): `Lifecycle.cfg` + TLC, trybuild use-after-shutdown, extend proptest; honest `proofs/README.adoc` + `spectral_radius` misnomer note. - [ ] Estate Trustfile (v2026.2.5-final) from the standards baseline. - [ ] RSR: substrate → canonical `descriptiles/`, GOVERNANCE.adoc, k9 validators. - [ ] Docs refresh (stale `.claude/CLAUDE.md`), STATE.a2ml, PMPL→MPL-2.0 correction. ## FLAGS — external / owner-plane (cannot resolve here) - **`gossamer` + `conative-gating` clones are 403-blocked** by the egress policy, so the #83 migration epic (#109, #111#115) and #103 egress-veto are **staged** behind access being granted at the platform level. Only the gossamer-independent #110 proceeds. - **Org-plane CI:** Actions publish/OIDC, `HYPATIA_SCAN_PAT`, Scorecard branch-protection / code-review. If "OpenSSF Scorecard Enforcer" is still a *required* status check, its context is now retired — please drop it from branch protection. - **OPEN proof obligations:** 1.1 (ESP formal contraction), 1.2 (LSM formal bound), 2.2, 3.1 residual — honest, with completion paths; nothing faked. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0172RBMz3qYjb1ttzD2i7RNh --- _Generated by [Claude Code](https://claude.ai/code/session_0172RBMz3qYjb1ttzD2i7RNh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 85a32e2 commit c786bb2

8 files changed

Lines changed: 665 additions & 128 deletions

File tree

.github/workflows/scorecard-enforcer.yml

Lines changed: 0 additions & 114 deletions
This file was deleted.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/neurophone-android/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ name = "neurophone-android"
33
description = "NeuroPhone Android JNI Bindings"
44
version.workspace = true
55
edition.workspace = true
6+
license.workspace = true
7+
authors.workspace = true
8+
repository.workspace = true
69

710
[lib]
8-
crate-type = ["cdylib"]
11+
# "lib" (rlib) so host `cargo test` can build+run the bridge unit tests;
12+
# "cdylib" for the actual Android JNI shared object.
13+
crate-type = ["lib", "cdylib"]
914

1015
[dependencies]
1116
neurophone-core = { path = "../neurophone-core" }
@@ -14,6 +19,7 @@ sensors = { path = "../sensors" }
1419
jni = { workspace = true }
1520
serde = { workspace = true }
1621
serde_json = { workspace = true }
22+
thiserror = { workspace = true }
1723
tracing = { workspace = true }
1824
tracing-subscriber = { workspace = true }
1925
tokio = { workspace = true }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
//! Error type bridging `neurophone-core` and `jni` failures across the boundary.
3+
4+
use thiserror::Error;
5+
6+
/// Unified error for the JNI bridge.
7+
///
8+
/// The `jni` `with_env`/`resolve` machinery only requires the closure error to
9+
/// implement [`std::error::Error`]; this enum lets a single `?` propagate both
10+
/// JNI failures and core failures, which `ThrowRuntimeExAndDefault` then turns
11+
/// into a Java `RuntimeException` (never an unwind across FFI).
12+
#[derive(Error, Debug)]
13+
pub enum JniBridgeError {
14+
/// A failure raised by the `jni` crate itself.
15+
#[error(transparent)]
16+
Jni(#[from] jni::errors::Error),
17+
/// A failure raised by `neurophone-core`.
18+
#[error(transparent)]
19+
Core(#[from] neurophone_core::NeurophoneError),
20+
/// A bridge-layer precondition failure (e.g. "not initialised").
21+
#[error("{0}")]
22+
Msg(String),
23+
}
Lines changed: 211 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,214 @@
11
// SPDX-License-Identifier: MPL-2.0
2-
//! Stub implementation
2+
//! NeuroPhone Android JNI bindings — class `ai.neurophone.NativeLib`.
3+
//!
4+
//! This is a pure ABI boundary: every `#[no_mangle]` export marshals JNI
5+
//! arguments to/from Rust and delegates to [`state`], which holds the live
6+
//! [`neurophone_core::NeuroSymbolicSystem`] and contains all bridge logic. Under
7+
//! jni 0.22, native methods receive an [`EnvUnowned`] and call `with_env`, which
8+
//! wraps the body in `catch_unwind` so a panic can never unwind across FFI; the
9+
//! [`ThrowRuntimeExAndDefault`] policy converts any error into a Java
10+
//! `RuntimeException`.
11+
//!
12+
//! There is no unsafe *code* here — every JVM interaction goes through jni's safe
13+
//! `with_env`/`resolve` API. The crate cannot use `#![forbid(unsafe_code)]` only
14+
//! because the JNI ABI export attribute is spelled `#[unsafe(no_mangle)]`; this is
15+
//! the estate-sanctioned FFI boundary crate (Trust `[FFI]` policy), and the
16+
//! exports are the sole reason the attribute appears.
17+
//!
18+
//! The JNI surface mirrors the contract in `docs/migrations/JNI-SURFACE-AUDIT.adoc`
19+
//! (11 methods). It is gossamer-independent: gossamer changes *who* loads and
20+
//! calls this cdylib, not this ABI.
321
4-
pub fn hello() -> &'static str {
5-
"hello"
22+
mod error;
23+
mod sensor_map;
24+
mod state;
25+
26+
use error::JniBridgeError;
27+
use jni::errors::ThrowRuntimeExAndDefault;
28+
use jni::objects::{JClass, JFloatArray, JString};
29+
use jni::refs::Reference;
30+
use jni::sys::{jboolean, jint, jlong};
31+
use jni::EnvUnowned;
32+
use neurophone_core::QueryRoute;
33+
34+
// jni 0.22 models `jboolean` as a real Rust `bool`.
35+
const JNI_TRUE: jboolean = true;
36+
37+
/// `init(configJson: String?): Boolean`
38+
#[unsafe(no_mangle)]
39+
pub extern "system" fn Java_ai_neurophone_NativeLib_init<'c>(
40+
mut unowned_env: EnvUnowned<'c>,
41+
_class: JClass<'c>,
42+
config_json: JString<'c>,
43+
) -> jboolean {
44+
unowned_env
45+
.with_env(|_env| -> Result<jboolean, JniBridgeError> {
46+
let json = if config_json.is_null() {
47+
None
48+
} else {
49+
Some(config_json.to_string())
50+
};
51+
state::init(json.as_deref())?;
52+
Ok(JNI_TRUE)
53+
})
54+
.resolve::<ThrowRuntimeExAndDefault>()
55+
}
56+
57+
/// `start(): Boolean`
58+
#[unsafe(no_mangle)]
59+
pub extern "system" fn Java_ai_neurophone_NativeLib_start<'c>(
60+
mut unowned_env: EnvUnowned<'c>,
61+
_class: JClass<'c>,
62+
) -> jboolean {
63+
unowned_env
64+
.with_env(|_env| -> Result<jboolean, JniBridgeError> {
65+
state::start()?;
66+
Ok(JNI_TRUE)
67+
})
68+
.resolve::<ThrowRuntimeExAndDefault>()
69+
}
70+
71+
/// `stop()`
72+
#[unsafe(no_mangle)]
73+
pub extern "system" fn Java_ai_neurophone_NativeLib_stop<'c>(
74+
mut unowned_env: EnvUnowned<'c>,
75+
_class: JClass<'c>,
76+
) {
77+
unowned_env
78+
.with_env(|_env| -> Result<(), JniBridgeError> {
79+
state::stop();
80+
Ok(())
81+
})
82+
.resolve::<ThrowRuntimeExAndDefault>()
83+
}
84+
85+
/// `isRunning(): Boolean`
86+
#[unsafe(no_mangle)]
87+
pub extern "system" fn Java_ai_neurophone_NativeLib_isRunning<'c>(
88+
mut unowned_env: EnvUnowned<'c>,
89+
_class: JClass<'c>,
90+
) -> jboolean {
91+
unowned_env
92+
.with_env(|_env| -> Result<jboolean, JniBridgeError> { Ok(state::is_running()) })
93+
.resolve::<ThrowRuntimeExAndDefault>()
94+
}
95+
96+
/// `processSensor(sensorType: Int, values: FloatArray, timestamp: Long, accuracy: Int): Boolean`
97+
///
98+
/// `accuracy` is accepted for ABI stability and intentionally ignored (the core
99+
/// does not consume it).
100+
#[unsafe(no_mangle)]
101+
pub extern "system" fn Java_ai_neurophone_NativeLib_processSensor<'c>(
102+
mut unowned_env: EnvUnowned<'c>,
103+
_class: JClass<'c>,
104+
sensor_type: jint,
105+
values: JFloatArray<'c>,
106+
timestamp: jlong,
107+
_accuracy: jint,
108+
) -> jboolean {
109+
unowned_env
110+
.with_env(|env| -> Result<jboolean, JniBridgeError> {
111+
let name = sensor_map::name_from_id(sensor_type)
112+
.ok_or_else(|| JniBridgeError::Msg(format!("unknown sensor id {sensor_type}")))?;
113+
let len = values.len(env)?;
114+
let mut buf = vec![0f32; len];
115+
values.get_region(env, 0, &mut buf)?;
116+
state::process_sensor(name, buf, timestamp.max(0) as u64)?;
117+
Ok(JNI_TRUE)
118+
})
119+
.resolve::<ThrowRuntimeExAndDefault>()
120+
}
121+
122+
/// `query(message: String, preferLocal: Boolean): String`
123+
#[unsafe(no_mangle)]
124+
pub extern "system" fn Java_ai_neurophone_NativeLib_query<'c>(
125+
mut unowned_env: EnvUnowned<'c>,
126+
_class: JClass<'c>,
127+
message: JString<'c>,
128+
prefer_local: jboolean,
129+
) -> JString<'c> {
130+
unowned_env
131+
.with_env(|env| -> Result<_, JniBridgeError> {
132+
let msg = message.to_string();
133+
let route = if prefer_local {
134+
QueryRoute::Auto
135+
} else {
136+
QueryRoute::ForceCloud
137+
};
138+
let resp = state::query(&msg, route)?;
139+
Ok(JString::from_str(env, resp)?)
140+
})
141+
.resolve::<ThrowRuntimeExAndDefault>()
142+
}
143+
144+
/// `queryLocal(message: String): String` — forces the local model.
145+
#[unsafe(no_mangle)]
146+
pub extern "system" fn Java_ai_neurophone_NativeLib_queryLocal<'c>(
147+
mut unowned_env: EnvUnowned<'c>,
148+
_class: JClass<'c>,
149+
message: JString<'c>,
150+
) -> JString<'c> {
151+
unowned_env
152+
.with_env(|env| -> Result<_, JniBridgeError> {
153+
let resp = state::query(&message.to_string(), QueryRoute::ForceLocal)?;
154+
Ok(JString::from_str(env, resp)?)
155+
})
156+
.resolve::<ThrowRuntimeExAndDefault>()
157+
}
158+
159+
/// `queryClaude(message: String): String` — forces the cloud model.
160+
#[unsafe(no_mangle)]
161+
pub extern "system" fn Java_ai_neurophone_NativeLib_queryClaude<'c>(
162+
mut unowned_env: EnvUnowned<'c>,
163+
_class: JClass<'c>,
164+
message: JString<'c>,
165+
) -> JString<'c> {
166+
unowned_env
167+
.with_env(|env| -> Result<_, JniBridgeError> {
168+
let resp = state::query(&message.to_string(), QueryRoute::ForceCloud)?;
169+
Ok(JString::from_str(env, resp)?)
170+
})
171+
.resolve::<ThrowRuntimeExAndDefault>()
172+
}
173+
174+
/// `getNeuralContext(): String`
175+
#[unsafe(no_mangle)]
176+
pub extern "system" fn Java_ai_neurophone_NativeLib_getNeuralContext<'c>(
177+
mut unowned_env: EnvUnowned<'c>,
178+
_class: JClass<'c>,
179+
) -> JString<'c> {
180+
unowned_env
181+
.with_env(|env| -> Result<_, JniBridgeError> {
182+
let ctx = state::neural_context()?;
183+
Ok(JString::from_str(env, ctx)?)
184+
})
185+
.resolve::<ThrowRuntimeExAndDefault>()
186+
}
187+
188+
/// `getState(): String` — the `SystemState` as JSON.
189+
#[unsafe(no_mangle)]
190+
pub extern "system" fn Java_ai_neurophone_NativeLib_getState<'c>(
191+
mut unowned_env: EnvUnowned<'c>,
192+
_class: JClass<'c>,
193+
) -> JString<'c> {
194+
unowned_env
195+
.with_env(|env| -> Result<_, JniBridgeError> {
196+
let json = state::state_json()?;
197+
Ok(JString::from_str(env, json)?)
198+
})
199+
.resolve::<ThrowRuntimeExAndDefault>()
200+
}
201+
202+
/// `reset()`
203+
#[unsafe(no_mangle)]
204+
pub extern "system" fn Java_ai_neurophone_NativeLib_reset<'c>(
205+
mut unowned_env: EnvUnowned<'c>,
206+
_class: JClass<'c>,
207+
) {
208+
unowned_env
209+
.with_env(|_env| -> Result<(), JniBridgeError> {
210+
state::reset()?;
211+
Ok(())
212+
})
213+
.resolve::<ThrowRuntimeExAndDefault>()
6214
}

0 commit comments

Comments
 (0)