// End-to-end kernel test: drive a server reading from the kernel side of a // mock_pair, while the test acts as the agent on the other side. Validates // the request/response loop, policy gating, and audit recording. #include "glove/audit/event.hpp" #include "glove/audit/sink.hpp" #include "glove/kernel/extension.hpp" #include "glove/kernel/registry.hpp" #include "glove/kernel/server.hpp" #include "glove/mcp/messages.hpp" #include "glove/policy/decision.hpp" #include "glove/policy/engine.hpp" #include "tests/kernel/mock_pair.hpp" #include #include #include #include #include #include #include namespace { #define REQUIRE(cond) \ do { \ if (!(cond)) { \ std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #cond, __FILE__, __LINE__); \ return 1; \ } \ } while (0) class echo_extension final : public glove::kernel::extension { public: auto name() const -> std::string_view override { return "echo"; } auto tools() -> std::expected, std::string> override { return std::vector{ {.name = "say", .description = "Echo the text argument back as content.", .input_schema_json = R"({"type":"object","properties":{"text":{"type":"string"}}})", .annotations = {}}, {.name = "secret", .description = "Must not be advertised when policy denies it.", .input_schema_json = R"({"type":"object"})", .annotations = {}}, }; } auto invoke(std::string_view tool_name, std::string_view arguments_json) -> std::expected override { if (tool_name != "say") { return std::unexpected(std::string{"unknown tool"}); } return glove::mcp::tool_call_result{ .status = glove::mcp::tool_call_status::ok, .content = "echoed", .structured_json = std::string{arguments_json}, .error_message = "", }; } }; auto run() -> int { glove::testing::mock_pair pair; glove::kernel::registry reg; REQUIRE(reg.add(std::make_unique()).has_value()); // Policy operates on the qualified tool name as the agent sees it. auto policy_eng = glove::policy::make_allow_list_engine( {.allow = {"echo.say"}, .deny = {"echo.forbidden"}, .default_decision = glove::policy::decision::deny} ); auto sink = glove::audit::make_memory_sink(); glove::kernel::server::options opts{ .identity = {.name = "glove-test", .version = "0.0.1"}, .policy = std::move(policy_eng), .audit = sink, }; glove::kernel::server srv{pair.kernel_side(), reg, std::move(opts)}; std::thread server_thread{[&] { (void)srv.run(); }}; auto& agent = pair.agent_side(); // initialize REQUIRE( agent .send( R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}})" ) .has_value() ); auto reply = agent.recv(); REQUIRE(reply.has_value()); REQUIRE(reply->find("\"id\":1") != std::string::npos); REQUIRE(reply->find("\"name\":\"glove-test\"") != std::string::npos); REQUIRE(reply->find("2025-06-18") != std::string::npos); // tools/list REQUIRE(agent.send(R"({"jsonrpc":"2.0","id":2,"method":"tools/list"})").has_value()); reply = agent.recv(); REQUIRE(reply.has_value()); REQUIRE(reply->find("\"id\":2") != std::string::npos); REQUIRE(reply->find("\"name\":\"echo.say\"") != std::string::npos); REQUIRE(reply->find("echo.secret") == std::string::npos); // tools/call allowed REQUIRE( agent .send( R"({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo.say","arguments":{"text":"hi"}}})" ) .has_value() ); reply = agent.recv(); REQUIRE(reply.has_value()); REQUIRE(reply->find("\"id\":3") != std::string::npos); REQUIRE(reply->find("\"text\":\"hi\"") != std::string::npos); // tools/call denied by policy (echo.forbidden does not exist as a tool, // but it doesn't matter — policy denies before dispatch; we just need a // name that's not in the allow list). REQUIRE( agent .send( R"({"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"echo.unknown_tool","arguments":{}}})" ) .has_value() ); reply = agent.recv(); REQUIRE(reply.has_value()); REQUIRE(reply->find("\"id\":4") != std::string::npos); REQUIRE(reply->find("policy denied") != std::string::npos); // unknown method REQUIRE(agent.send(R"({"jsonrpc":"2.0","id":5,"method":"resources/list"})").has_value()); reply = agent.recv(); REQUIRE(reply.has_value()); REQUIRE(reply->find("\"id\":5") != std::string::npos); REQUIRE(reply->find("-32601") != std::string::npos); // notification: no response should appear. REQUIRE(agent.send(R"({"jsonrpc":"2.0","method":"notifications/initialized"})").has_value()); // Close the agent → kernel direction so the server exits. pair.close_agent_to_kernel(); server_thread.join(); auto events = sink->take(); // Expected: initialize, tools/list, tools/call OK, tools/call DENIED. REQUIRE(events.size() >= 4); REQUIRE(events[0].what == glove::audit::action::initialize); REQUIRE(events[1].what == glove::audit::action::list_tools); REQUIRE(events[2].what == glove::audit::action::call_tool); REQUIRE(events[2].status == glove::mcp::tool_call_status::ok); REQUIRE(events[3].what == glove::audit::action::call_tool); REQUIRE(events[3].status == glove::mcp::tool_call_status::invalid_arguments); REQUIRE(events[3].error_message.find("policy denied") != std::string::npos); return 0; } } // namespace auto main() -> int { return run(); }