mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 16:53:04 +08:00
feat(runner): implement BubbleWrap policy provider and related test
This commit is contained in:
@@ -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 注册完毕");
|
||||||
|
|||||||
@@ -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'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user