diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml
new file mode 100644
index 00000000..6e9e886f
--- /dev/null
+++ b/.github/workflows/bindings.yml
@@ -0,0 +1,247 @@
+name: Bindings
+
+on:
+ push:
+ branches: [ master ]
+ paths:
+ - 'bindings/**'
+ - 'include/naab/public/**'
+ - 'src/api/**'
+ - 'CMakeLists.txt'
+ pull_request:
+ branches: [ master ]
+ paths:
+ - 'bindings/**'
+ - 'include/naab/public/**'
+ - 'src/api/**'
+ - 'CMakeLists.txt'
+ workflow_dispatch:
+
+jobs:
+ build-shared-lib:
+ name: Build libnaab-governance
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ cmake ninja-build libsqlite3-dev python3-dev \
+ libssl-dev libffi-dev libcurl4-openssl-dev pkg-config
+
+ - name: Build shared library
+ run: |
+ cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja
+ ninja -C build naab_governance naab-gov -j$(nproc)
+ # Verify shared lib was built
+ ls -la build/libnaab-governance.so*
+
+ - name: Prepare artifacts
+ run: |
+ mkdir -p artifacts
+ # Copy the real .so file (not symlinks — artifacts don't preserve them)
+ cp build/libnaab-governance.so.*.* artifacts/libnaab-governance.so 2>/dev/null || \
+ cp build/libnaab-governance.so artifacts/libnaab-governance.so
+ cp build/naab-gov artifacts/
+ ls -la artifacts/
+
+ - name: Upload shared library
+ uses: actions/upload-artifact@v4
+ with:
+ name: libnaab-governance
+ path: artifacts/
+ retention-days: 1
+
+ test-go:
+ name: Go Bindings
+ needs: build-shared-lib
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version: '1.21'
+ cache: false
+
+ - name: Download shared library
+ uses: actions/download-artifact@v4
+ with:
+ name: libnaab-governance
+ path: build
+
+ - name: Install shared library
+ run: |
+ sudo cp build/libnaab-governance.so /usr/local/lib/
+ sudo ldconfig
+
+ - name: Run Go tests
+ working-directory: bindings/go
+ run: |
+ CGO_LDFLAGS="-L/usr/local/lib" \
+ go test -v ./naabgov/
+
+ test-rust:
+ name: Rust Bindings
+ needs: build-shared-lib
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - name: Download shared library
+ uses: actions/download-artifact@v4
+ with:
+ name: libnaab-governance
+ path: build
+
+ - name: Install shared library
+ run: |
+ sudo cp build/libnaab-governance.so /usr/local/lib/
+ sudo ldconfig
+
+ - name: Run Rust tests
+ working-directory: bindings/rust
+ run: |
+ NAAB_LIB_DIR=/usr/local/lib cargo test --verbose
+
+ test-java:
+ name: Java Bindings
+ needs: build-shared-lib
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Download shared library
+ uses: actions/download-artifact@v4
+ with:
+ name: libnaab-governance
+ path: build
+
+ - name: Install shared library
+ run: |
+ sudo cp build/libnaab-governance.so /usr/local/lib/
+ sudo ldconfig
+
+ - name: Build JNI bridge
+ working-directory: bindings/java
+ run: |
+ # Compile Java source
+ javac -d classes src/main/java/org/naab/governance/GovernanceEngine.java
+
+ # Build JNI shared library
+ cc -shared -fPIC -o libnaab-governance-jni.so \
+ src/main/c/naab_gov_jni.c \
+ -I${JAVA_HOME}/include \
+ -I${JAVA_HOME}/include/linux \
+ -I../../include/naab/public \
+ -L/usr/local/lib \
+ -lnaab-governance
+
+ sudo cp libnaab-governance-jni.so /usr/local/lib/
+ sudo ldconfig
+
+ - name: Compile tests
+ working-directory: bindings/java
+ run: |
+ javac -d classes -cp classes \
+ src/test/java/org/naab/governance/GovernanceEngineTest.java
+
+ - name: Run Java tests
+ working-directory: bindings/java
+ run: |
+ java -cp classes \
+ -Djava.library.path=/usr/local/lib \
+ org.naab.governance.GovernanceEngineTest
+
+ test-csharp:
+ name: C# Bindings
+ needs: build-shared-lib
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0'
+
+ - name: Download shared library
+ uses: actions/download-artifact@v4
+ with:
+ name: libnaab-governance
+ path: build
+
+ - name: Install shared library
+ run: |
+ sudo cp build/libnaab-governance.so /usr/local/lib/
+ sudo ldconfig
+
+ - name: Run C# tests
+ working-directory: bindings/csharp
+ run: |
+ LD_LIBRARY_PATH=/usr/local/lib dotnet test NaabGovernance.Tests/ --verbosity normal
+
+ test-python:
+ name: Python Bindings (subprocess)
+ needs: build-shared-lib
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Download shared library
+ uses: actions/download-artifact@v4
+ with:
+ name: libnaab-governance
+ path: build
+
+ - name: Install shared library and CLI
+ run: |
+ sudo cp build/libnaab-governance.so /usr/local/lib/
+ sudo ldconfig
+ chmod +x build/naab-gov
+
+ - name: Run Python binding smoke test
+ run: |
+ python3 -c "
+ import sys, os
+ sys.path.insert(0, 'bindings/python')
+
+ # Test 1: ctypes mode (shared lib available)
+ os.environ['NAAB_GOV_LIB'] = os.path.abspath('build/libnaab-governance.so')
+ from naab_governance import GovernanceEngine
+
+ eng = GovernanceEngine()
+ assert not eng._subprocess_mode, 'should be in ctypes mode'
+ eng.load_config_string('{\"version\":\"5.0\",\"mode\":\"enforce\"}')
+ result = eng.scan('python', 'x = 42')
+ assert not result.get('blocked'), 'safe code should not be blocked'
+ del eng
+ print('Python ctypes binding: OK')
+
+ # Test 2: subprocess mode (force by pointing to nonexistent lib)
+ os.environ['NAAB_GOV_LIB'] = '/nonexistent'
+ os.environ['NAAB_GOV_BIN'] = os.path.abspath('build/naab-gov')
+ # Re-import to get fresh instance
+ import importlib, naab_governance
+ importlib.reload(naab_governance)
+ eng2 = naab_governance.GovernanceEngine(lib_path='/nonexistent')
+ assert eng2._subprocess_mode, 'should be in subprocess mode'
+ eng2.load_config_dict({'version': '5.0', 'mode': 'enforce'})
+ result2 = eng2.scan('python', 'x = 42')
+ assert 'blocked' in result2, 'result should have blocked field'
+ print('Python subprocess binding: OK')
+ "
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 82b649a2..76ad428e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -91,9 +91,11 @@ if(ENABLE_HARDENING AND NOT MSVC)
# Stack protection
add_compile_options(-fstack-protector-strong)
- # Position-independent code (PIE) for ASLR
- add_compile_options(-fPIE)
- add_link_options(-pie)
+ # Position-independent code for ASLR
+ # CMAKE_POSITION_INDEPENDENT_CODE=ON (line 33) already adds -fPIC globally,
+ # which is a superset of -fPIE. We only need -pie for the linker on executables
+ # (-pie conflicts with -shared, so we scope it to CMAKE_EXE_LINKER_FLAGS).
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie")
# Fortify source (buffer overflow detection) — requires -O1+, inactive in Debug
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
diff --git a/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs b/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs
new file mode 100644
index 00000000..32872ad7
--- /dev/null
+++ b/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs
@@ -0,0 +1,77 @@
+using Xunit;
+using NaabGovernance;
+
+namespace NaabGovernance.Tests;
+
+public class GovernanceEngineTests
+{
+ private const string TestConfig =
+ @"{""version"":""3.0"",""mode"":""enforce"",
+ ""restrictions"":{""dangerous_calls"":{""level"":""hard""},
+ ""code_injection"":{""level"":""hard""}},
+ ""code_quality"":{""no_secrets"":{""level"":""hard""}}}";
+
+ [Fact]
+ public void Version_ReturnsNonEmpty()
+ {
+ var v = GovernanceEngine.Version;
+ Assert.False(string.IsNullOrEmpty(v));
+ }
+
+ [Fact]
+ public void Lifecycle_CreateAndDispose()
+ {
+ using var engine = new GovernanceEngine();
+ Assert.NotNull(engine);
+ }
+
+ [Fact]
+ public void Lifecycle_DoubleDispose()
+ {
+ var engine = new GovernanceEngine();
+ engine.Dispose();
+ engine.Dispose(); // should not throw
+ }
+
+ [Fact]
+ public void ScanSafeCode_NotBlocked()
+ {
+ using var engine = new GovernanceEngine();
+ engine.LoadConfigString(TestConfig);
+ Assert.True(engine.IsActive);
+
+ var result = engine.Scan("python", "x = 42\nprint(x)", "test.py", 1);
+ Assert.NotNull(result);
+ Assert.False(engine.WasBlocked);
+ }
+
+ [Fact]
+ public void ScanDangerousCode_Blocked()
+ {
+ using var engine = new GovernanceEngine();
+ engine.LoadConfigString(TestConfig);
+
+ engine.Scan("python", "import os; os.system('rm -rf /')", "test.py", 1);
+ Assert.True(engine.WasBlocked);
+ }
+
+ [Fact]
+ public void Reset_ClearsResults()
+ {
+ using var engine = new GovernanceEngine();
+ engine.LoadConfigString(TestConfig);
+
+ engine.Scan("python", "eval(input())", "test.py", 1);
+ Assert.True(engine.ResultCount > 0);
+
+ engine.Reset();
+ Assert.Equal(0, engine.ResultCount);
+ }
+
+ [Fact]
+ public void IsActive_FalseBeforeConfig()
+ {
+ using var engine = new GovernanceEngine();
+ Assert.False(engine.IsActive);
+ }
+}
diff --git a/bindings/csharp/NaabGovernance.Tests/NaabGovernance.Tests.csproj b/bindings/csharp/NaabGovernance.Tests/NaabGovernance.Tests.csproj
new file mode 100644
index 00000000..ff1a6320
--- /dev/null
+++ b/bindings/csharp/NaabGovernance.Tests/NaabGovernance.Tests.csproj
@@ -0,0 +1,15 @@
+
+
+ net8.0
+ false
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/bindings/java/src/main/c/naab_gov_jni.c b/bindings/java/src/main/c/naab_gov_jni.c
index 09fa2d56..7e1184b8 100644
--- a/bindings/java/src/main/c/naab_gov_jni.c
+++ b/bindings/java/src/main/c/naab_gov_jni.c
@@ -8,6 +8,7 @@
*/
#include
+#include
#include
#include "naab_governance.h"
diff --git a/bindings/java/src/test/java/org/naab/governance/GovernanceEngineTest.java b/bindings/java/src/test/java/org/naab/governance/GovernanceEngineTest.java
new file mode 100644
index 00000000..dc3280e0
--- /dev/null
+++ b/bindings/java/src/test/java/org/naab/governance/GovernanceEngineTest.java
@@ -0,0 +1,118 @@
+package org.naab.governance;
+
+/**
+ * Tests for NAAb governance JNI bindings.
+ *
+ * Runs standalone (no JUnit dependency) — each test method prints PASS/FAIL.
+ * Exit code 0 = all pass, 1 = any fail.
+ */
+public class GovernanceEngineTest {
+
+ private static final String TEST_CONFIG =
+ "{\"version\":\"3.0\",\"mode\":\"enforce\"," +
+ "\"restrictions\":{\"dangerous_calls\":{\"level\":\"hard\"}," +
+ "\"code_injection\":{\"level\":\"hard\"}}," +
+ "\"code_quality\":{\"no_secrets\":{\"level\":\"hard\"}}}";
+
+ private static int passed = 0;
+ private static int failed = 0;
+
+ private static void check(String name, boolean condition) {
+ if (condition) {
+ System.out.println(" PASS: " + name);
+ passed++;
+ } else {
+ System.out.println(" FAIL: " + name);
+ failed++;
+ }
+ }
+
+ public static void main(String[] args) {
+ System.out.println();
+ System.out.println("--- Java JNI Binding Tests ---");
+ System.out.println();
+
+ testVersion();
+ testLifecycle();
+ testScanSafeCode();
+ testScanDangerousCode();
+ testReset();
+ testIsActive();
+ testResultCount();
+
+ System.out.println();
+ System.out.println("Results: " + passed + "/" + (passed + failed) + " passed");
+ System.out.println();
+ System.exit(failed > 0 ? 1 : 0);
+ }
+
+ private static void testVersion() {
+ String v = GovernanceEngine.version();
+ check("version not null", v != null);
+ check("version not empty", v != null && !v.isEmpty());
+ }
+
+ private static void testLifecycle() {
+ try (GovernanceEngine engine = new GovernanceEngine()) {
+ check("create engine", engine != null);
+ }
+ // double close via try-with-resources then explicit — should not crash
+ boolean doubleCloseOk = false;
+ try {
+ GovernanceEngine engine = new GovernanceEngine();
+ engine.close();
+ engine.close();
+ doubleCloseOk = true;
+ } catch (Exception e) {
+ // native crash that propagates as Java exception
+ }
+ check("double close safe", doubleCloseOk);
+ }
+
+ private static void testScanSafeCode() {
+ try (GovernanceEngine engine = new GovernanceEngine()) {
+ engine.loadConfigString(TEST_CONFIG);
+ check("isActive after config", engine.isActive());
+
+ String result = engine.scan("python", "x = 42\nprint(x)", "test.py", 1);
+ check("scan returns JSON", result != null && result.contains("blocked"));
+ check("safe code not blocked", !engine.wasBlocked());
+ }
+ }
+
+ private static void testScanDangerousCode() {
+ try (GovernanceEngine engine = new GovernanceEngine()) {
+ engine.loadConfigString(TEST_CONFIG);
+ engine.scan("python", "import os; os.system('rm -rf /')", "test.py", 1);
+ check("dangerous code blocked", engine.wasBlocked());
+ }
+ }
+
+ private static void testReset() {
+ try (GovernanceEngine engine = new GovernanceEngine()) {
+ engine.loadConfigString(TEST_CONFIG);
+ engine.scan("python", "eval(input())", "test.py", 1);
+ check("has results after scan", engine.resultCount() > 0);
+
+ engine.reset();
+ check("no results after reset", engine.resultCount() == 0);
+ }
+ }
+
+ private static void testIsActive() {
+ try (GovernanceEngine engine = new GovernanceEngine()) {
+ check("not active before config", !engine.isActive());
+ engine.loadConfigString(TEST_CONFIG);
+ check("active after config", engine.isActive());
+ }
+ }
+
+ private static void testResultCount() {
+ try (GovernanceEngine engine = new GovernanceEngine()) {
+ engine.loadConfigString(TEST_CONFIG);
+ check("zero results before scan", engine.resultCount() == 0);
+ engine.scan("python", "eval(input())", "test.py", 1);
+ check("has results after scan", engine.resultCount() > 0);
+ }
+ }
+}
diff --git a/bindings/python/naab_governance.py b/bindings/python/naab_governance.py
index ab92b422..03884fa4 100644
--- a/bindings/python/naab_governance.py
+++ b/bindings/python/naab_governance.py
@@ -244,29 +244,16 @@ def _subprocess_scan(self, language: str, code: str, config=None) -> dict:
"""Run governance scan via naab-gov CLI subprocess."""
cmd = [self._subprocess_bin, "check", "--language", language]
if config:
- # Write temp config file
- import tempfile
- tmpdir = os.environ.get("TMPDIR", "/tmp")
- cfg_fd = tempfile.NamedTemporaryFile(
- mode="w", delete=False, dir=tmpdir, suffix=".json", prefix="naab_gov_"
- )
- cfg_path = cfg_fd.name
- try:
- json.dump(config if isinstance(config, dict) else json.loads(config), cfg_fd)
- cfg_fd.close()
- cmd.extend(["--config", cfg_path])
- proc = subprocess.run(
- cmd, input=code, capture_output=True, text=True, timeout=30
- )
- finally:
- try:
- os.unlink(cfg_path)
- except OSError:
- pass
- else:
- proc = subprocess.run(
- cmd, input=code, capture_output=True, text=True, timeout=30
+ # Pass config inline via --config-string to avoid signature
+ # verification issues with temp files (Ed25519 .sig required
+ # when trusted keys are installed)
+ cfg_json = json.dumps(
+ config if isinstance(config, dict) else json.loads(config)
)
+ cmd.extend(["--config-string", cfg_json])
+ proc = subprocess.run(
+ cmd, input=code, capture_output=True, text=True, timeout=30
+ )
if proc.returncode == 1:
raise RuntimeError(f"naab-gov check failed: {proc.stderr.strip()}")
if proc.returncode == 4:
diff --git a/bindings/rust/tests/governance_test.rs b/bindings/rust/tests/governance_test.rs
new file mode 100644
index 00000000..62432f4d
--- /dev/null
+++ b/bindings/rust/tests/governance_test.rs
@@ -0,0 +1,76 @@
+use naab_governance::{GovernanceEngine, version};
+
+const TEST_CONFIG: &str = r#"{
+ "version": "3.0",
+ "mode": "enforce",
+ "restrictions": {
+ "dangerous_calls": {"level": "hard"},
+ "code_injection": {"level": "hard"}
+ },
+ "code_quality": {
+ "no_secrets": {"level": "hard"}
+ }
+}"#;
+
+#[test]
+fn test_version() {
+ let v = version();
+ assert!(!v.is_empty(), "version string should not be empty");
+}
+
+#[test]
+fn test_lifecycle() {
+ let engine = GovernanceEngine::new().expect("create engine");
+ drop(engine); // explicit drop should not panic
+}
+
+#[test]
+fn test_scan_safe_code() {
+ let engine = GovernanceEngine::new().unwrap();
+ engine.load_config_string(TEST_CONFIG).unwrap();
+ assert!(engine.is_active(), "engine should be active after loading config");
+
+ let result = engine.scan("python", "x = 42\nprint(x)", "test.py", 1).unwrap();
+ assert!(!result.blocked, "safe code should not be blocked");
+}
+
+#[test]
+fn test_scan_dangerous_code() {
+ let engine = GovernanceEngine::new().unwrap();
+ engine.load_config_string(TEST_CONFIG).unwrap();
+
+ let result = engine.scan("python", "import os; os.system('rm -rf /')", "test.py", 1).unwrap();
+ assert!(result.blocked, "dangerous code should be blocked");
+}
+
+#[test]
+fn test_reset() {
+ let engine = GovernanceEngine::new().unwrap();
+ engine.load_config_string(TEST_CONFIG).unwrap();
+
+ engine.scan("python", "eval(input())", "test.py", 1).unwrap();
+ assert!(engine.result_count() > 0, "should have results after scan");
+
+ engine.reset();
+ assert_eq!(engine.result_count(), 0, "should have no results after reset");
+}
+
+#[test]
+fn test_is_active() {
+ let engine = GovernanceEngine::new().unwrap();
+ assert!(!engine.is_active(), "should not be active before loading config");
+
+ engine.load_config_string(TEST_CONFIG).unwrap();
+ assert!(engine.is_active(), "should be active after loading config");
+}
+
+#[test]
+fn test_result_count() {
+ let engine = GovernanceEngine::new().unwrap();
+ engine.load_config_string(TEST_CONFIG).unwrap();
+
+ assert_eq!(engine.result_count(), 0, "no results before scan");
+
+ engine.scan("python", "eval(input())", "test.py", 1).unwrap();
+ assert!(engine.result_count() > 0, "should have results after scanning dangerous code");
+}
diff --git a/src/cli/gov_main.cpp b/src/cli/gov_main.cpp
index a62e0560..52e1d94e 100644
--- a/src/cli/gov_main.cpp
+++ b/src/cli/gov_main.cpp
@@ -72,6 +72,7 @@ static void printHelp() {
" --language Target language (required: python, javascript, go, ...)\n"
" --file Read code from file instead of stdin\n"
" --config Use this govern.json instead of auto-discovery\n"
+ " --config-string Use inline JSON config string\n"
" --sarif Output SARIF instead of JSON\n"
" --env Apply named environment overlay from govern.json\n"
"\n"
@@ -265,6 +266,7 @@ static int cmdCheck(const std::vector& args) {
std::string language;
std::string file_path;
std::string config_path;
+ std::string config_string;
std::string env_name;
bool sarif_output = false;
@@ -276,6 +278,8 @@ static int cmdCheck(const std::vector& args) {
file_path = args[++i];
} else if (args[i] == "--config" && i + 1 < args.size()) {
config_path = args[++i];
+ } else if (args[i] == "--config-string" && i + 1 < args.size()) {
+ config_string = args[++i];
} else if (args[i] == "--env" && i + 1 < args.size()) {
env_name = args[++i];
} else if (args[i] == "--sarif") {
@@ -286,6 +290,11 @@ static int cmdCheck(const std::vector& args) {
}
}
+ if (!config_string.empty() && !config_path.empty()) {
+ std::cerr << "naab-gov check: --config and --config-string are mutually exclusive\n";
+ return 4;
+ }
+
if (language.empty()) {
std::cerr << "naab-gov check: --language is required\n"
"Usage: naab-gov check --language [--file ] [--config govern.json]\n";
@@ -322,10 +331,25 @@ static int cmdCheck(const std::vector& args) {
}
// Load governance config
+ //
+ // Threat model for --config-string: naab-gov is a user-space CLI tool,
+ // never setuid/setgid, never runs with elevated privileges. The caller
+ // already has full control of the process (can pass --no-governance to
+ // naab-lang, or simply not invoke governance at all). --config-string
+ // exists for programmatic callers (Python subprocess binding) that need
+ // to pass config without writing temp files that require Ed25519 signing.
+ // Signature verification gates file-based configs; inline configs are
+ // trusted as caller-provided input, same as any other CLI argument.
naab::governance::GovernanceEngine engine;
bool loaded = false;
- if (!config_path.empty()) {
+ if (!config_string.empty()) {
+ loaded = engine.loadFromString(config_string);
+ if (!loaded) {
+ std::cerr << "naab-gov check: failed to parse config string\n";
+ return 4;
+ }
+ } else if (!config_path.empty()) {
if (!fs::exists(config_path)) {
std::cerr << "naab-gov check: govern.json not found: " << config_path << "\n";
return 4;
diff --git a/tests/cli/test_naab_gov.sh b/tests/cli/test_naab_gov.sh
index 11ac86d5..f6cedcab 100644
--- a/tests/cli/test_naab_gov.sh
+++ b/tests/cli/test_naab_gov.sh
@@ -75,6 +75,32 @@ MISSING_EXIT=$?
check "lint non-existent file returns non-zero" \
"$([ "${MISSING_EXIT}" -ne 0 ] && echo 0 || echo 1)"
+# Test 6: check --config-string passes JSON inline
+CONFIG_STR_OUT=$(echo "x = 1" | "${GOV}" check --language python --config-string '{"version":"5.0","mode":"enforce"}' 2>&1)
+CONFIG_STR_EXIT=$?
+check "--config-string exits 0" "$CONFIG_STR_EXIT"
+check "--config-string returns JSON" \
+ "$(echo "${CONFIG_STR_OUT}" | grep -q '"blocked"' && echo 0 || echo 1)"
+
+# Test 7: check --config-string with invalid JSON returns exit 4
+echo "x = 1" | "${GOV}" check --language python --config-string '{bad json' 2>/dev/null
+BADJSON_EXIT=$?
+check "--config-string bad JSON exits 4" \
+ "$([ "${BADJSON_EXIT}" -eq 4 ] && echo 0 || echo 1)"
+
+# Test 8: check --config-string actually applies config (dangerous code → exit 3)
+echo 'import os; os.system("rm -rf /")' | "${GOV}" check --language python \
+ --config-string '{"version":"5.0","mode":"enforce","restrictions":{"dangerous_calls":{"level":"hard"}}}' 2>/dev/null
+DANGEROUS_EXIT=$?
+check "--config-string enforces config (exit 3)" \
+ "$([ "${DANGEROUS_EXIT}" -eq 3 ] && echo 0 || echo 1)"
+
+# Test 9: --config and --config-string are mutually exclusive (exit 4)
+echo "x = 1" | "${GOV}" check --language python --config /dev/null --config-string '{}' 2>/dev/null
+MUTEX_EXIT=$?
+check "--config + --config-string mutually exclusive (exit 4)" \
+ "$([ "${MUTEX_EXIT}" -eq 4 ] && echo 0 || echo 1)"
+
echo ""
echo "Results: ${PASS}/${TESTS} passed"
echo ""