feat(runner): implement BubbleWrap policy provider and related test

This commit is contained in:
2026-03-17 12:11:54 +08:00
parent 1465d7687b
commit a6682a7719
4 changed files with 196 additions and 0 deletions

View File

@@ -11,6 +11,8 @@ import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
import work.slhaf.partner.core.action.runner.execution.McpActionExecutor; import work.slhaf.partner.core.action.runner.execution.McpActionExecutor;
import work.slhaf.partner.core.action.runner.execution.OriginExecutionService; import work.slhaf.partner.core.action.runner.execution.OriginExecutionService;
import work.slhaf.partner.core.action.runner.mcp.*; import work.slhaf.partner.core.action.runner.mcp.*;
import work.slhaf.partner.core.action.runner.policy.BwrapPolicyProvider;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.support.ActionSerializer; import work.slhaf.partner.core.action.runner.support.ActionSerializer;
import java.io.IOException; import java.io.IOException;
@@ -69,6 +71,8 @@ public class LocalRunnerClient extends RunnerClient implements AutoCloseable {
McpConfigWatcher configWatcher = null; McpConfigWatcher configWatcher = null;
try { try {
ExecutionPolicyRegistry.INSTANCE.registerPolicyProvider(BwrapPolicyProvider.INSTANCE);
metaRegistry = new McpMetaRegistry(existedMetaActions); metaRegistry = new McpMetaRegistry(existedMetaActions);
registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DESC, metaRegistry.clientConfig(MCP_NAME_DESC, 10)); registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DESC, metaRegistry.clientConfig(MCP_NAME_DESC, 10));
log.info("DescMcp 注册完毕"); log.info("DescMcp 注册完毕");

View File

@@ -0,0 +1,71 @@
package work.slhaf.partner.core.action.runner.policy
import work.slhaf.partner.core.action.exception.ActionInitFailedException
private const val BWRAP_COMMAND = "bwrap"
object BwrapPolicyProvider : PolicyProvider(
policyName = "bwrap"
) {
init {
requireBwrapAvailable()
}
override fun prepare(
policy: ExecutionPolicy,
commands: List<String>
): WrappedLaunchSpec {
val (command, args) = splitCommands(commands)
val wrappedArgs = buildList {
add("--ro-bind")
add("/")
add("/")
add("--proc")
add("/proc")
add("--dev")
add("/dev")
if (policy.net == ExecutionPolicy.Network.DISABLE) {
add("--unshare-net")
}
if (!policy.workingDirectory.isNullOrBlank()) {
add("--chdir")
add(policy.workingDirectory)
}
policy.readOnlyPaths.forEach { path ->
add("--ro-bind")
add(path)
add(path)
}
policy.writablePaths.forEach { path ->
add("--bind")
add(path)
add(path)
}
add("--")
add(command)
addAll(args)
}
return WrappedLaunchSpec(
command = BWRAP_COMMAND,
args = wrappedArgs,
workingDirectory = policy.workingDirectory,
environment = resolveEnvironment(policy)
)
}
private fun requireBwrapAvailable() {
val available = try {
val process = ProcessBuilder(BWRAP_COMMAND, "--version")
.redirectErrorStream(true)
.start()
val exitCode = process.waitFor()
exitCode == 0
} catch (e: Exception) {
throw ActionInitFailedException("bwrap provider 初始化失败: 无法检测 $BWRAP_COMMAND 可执行文件", e)
}
if (!available) {
throw ActionInitFailedException("bwrap provider 初始化失败: 未检测到可执行命令 '$BWRAP_COMMAND'")
}
}
}

View File

@@ -8,6 +8,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo; import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import work.slhaf.partner.module.modules.action.builtin.BuiltinActionRegistry; import work.slhaf.partner.module.modules.action.builtin.BuiltinActionRegistry;
import java.io.IOException; import java.io.IOException;
@@ -383,6 +386,38 @@ public class LocalRunnerClientTest {
} }
} }
@Test
void testLocalRunnerClientRegisterBwrapPolicyProvider(@TempDir Path tempDir) {
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
LocalRunnerClient ignored = new LocalRunnerClient(existedMetaActions, executor, tempDir.toString())) {
ExecutionPolicyRegistry.INSTANCE.updatePolicy(new ExecutionPolicy(
ExecutionPolicy.Mode.SANDBOX,
"bwrap",
ExecutionPolicy.Network.DISABLE,
false,
Map.of("A", "B"),
tempDir.toString(),
Set.of("/etc"),
Set.of(tempDir.toString())
));
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(List.of("python", "demo.py"));
Assertions.assertEquals("bwrap", wrapped.getCommand());
Assertions.assertTrue(wrapped.getArgs().contains("--unshare-net"));
} finally {
ExecutionPolicyRegistry.INSTANCE.updatePolicy(new ExecutionPolicy(
ExecutionPolicy.Mode.DIRECT,
"direct",
ExecutionPolicy.Network.ENABLE,
true,
Map.of(),
null,
Set.of(),
Set.of()
));
}
}
@Nested @Nested
class DescMcpTest { class DescMcpTest {

View File

@@ -0,0 +1,86 @@
package work.slhaf.partner.core.action.runner.policy
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import work.slhaf.partner.core.action.exception.ActionInitFailedException
class BwrapPolicyProviderTest {
@Test
fun `prepare wraps commands and policy settings`() {
val policy = ExecutionPolicy(
mode = ExecutionPolicy.Mode.SANDBOX,
provider = "bwrap",
net = ExecutionPolicy.Network.DISABLE,
inheritEnv = false,
env = mapOf("KEY" to "VALUE"),
workingDirectory = "/work/demo",
readOnlyPaths = setOf("/etc"),
writablePaths = setOf("/tmp/demo")
)
val wrapped = BwrapPolicyProvider.prepare(policy, listOf("python", "script.py", "--flag"))
assertEquals("bwrap", wrapped.command)
assertEquals("/work/demo", wrapped.workingDirectory)
assertEquals(mapOf("KEY" to "VALUE"), wrapped.environment)
assertEquals(
listOf(
"--ro-bind", "/", "/",
"--proc", "/proc",
"--dev", "/dev",
"--unshare-net",
"--chdir", "/work/demo",
"--ro-bind", "/etc", "/etc",
"--bind", "/tmp/demo", "/tmp/demo",
"--", "python", "script.py", "--flag"
),
wrapped.args
)
}
@Test
fun `prepare inherits environment`() {
val policy = ExecutionPolicy(
mode = ExecutionPolicy.Mode.SANDBOX,
provider = "bwrap",
net = ExecutionPolicy.Network.ENABLE,
inheritEnv = true,
env = mapOf("BWRAP_TEST_KEY" to "VALUE"),
workingDirectory = null,
readOnlyPaths = emptySet(),
writablePaths = emptySet()
)
val wrapped = BwrapPolicyProvider.prepare(policy, listOf("echo", "ok"))
assertEquals("VALUE", wrapped.environment["BWRAP_TEST_KEY"])
assertFalse(wrapped.environment.isEmpty())
assertFalse(wrapped.args.contains("--unshare-net"))
}
@Test
fun `require command available throws ActionInitFailedException when command missing`() {
val exception = assertThrows(ActionInitFailedException::class.java) {
bwrapPolicyFileFacadeClass().requireCommandAvailable("definitely-not-found-bwrap-command")
}
assertTrue(exception.message!!.contains("definitely-not-found-bwrap-command"))
}
}
private fun bwrapPolicyFileFacadeClass(): Class<*> {
return Class.forName("work.slhaf.partner.core.action.runner.policy.BwrapPolicyProviderKt")
}
private fun Class<*>.requireCommandAvailable(command: String) {
val method = getDeclaredMethod("requireCommandAvailable", String::class.java)
method.isAccessible = true
try {
method.invoke(null, command)
} catch (e: java.lang.reflect.InvocationTargetException) {
throw (e.targetException as? RuntimeException)
?: (e.targetException as? Error)
?: RuntimeException(e.targetException)
}
}