Step 125: agent permissions and request logging
This commit is contained in:
@@ -702,6 +702,10 @@ target_include_directories(step124_test PRIVATE src)
|
||||
target_link_libraries(step124_test PRIVATE
|
||||
nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step125_test tests/step125_test.cpp)
|
||||
target_include_directories(step125_test PRIVATE src)
|
||||
target_link_libraries(step125_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -157,6 +157,7 @@ struct EditorState {
|
||||
MockWebSocketTransport* agentTransport = nullptr;
|
||||
int agentPort = 8765;
|
||||
std::vector<std::string> agentLog;
|
||||
std::map<std::string, bool> agentMutationPermissions;
|
||||
|
||||
// Custom editor widget state
|
||||
bool showWhitespace = false;
|
||||
@@ -713,12 +714,25 @@ struct EditorState {
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
agentTransport = transport.get();
|
||||
agentServer = std::make_unique<WebSocketAgentServer>(std::move(transport));
|
||||
agentServer->setRequestHandler([this](const json& request) {
|
||||
return processAgentRequest(request);
|
||||
agentServer->setRequestHandler([this](const json& request,
|
||||
const std::string& sessionId) {
|
||||
return processAgentRequest(request, sessionId);
|
||||
});
|
||||
agentServer->setSessionEventCallback([this](const AgentSession& s,
|
||||
const std::string& event) {
|
||||
logAgentEvent("Agent " + s.sessionId + " " + event);
|
||||
if (event == "connected") {
|
||||
agentMutationPermissions[s.sessionId] = false;
|
||||
} else if (event == "disconnected") {
|
||||
agentMutationPermissions.erase(s.sessionId);
|
||||
}
|
||||
});
|
||||
agentServer->setRequestLogCallback([this](const std::string& sid,
|
||||
const json& req,
|
||||
const json& res) {
|
||||
std::string method = req.value("method", "");
|
||||
bool ok = !res.contains("error");
|
||||
logAgentEvent("RPC " + sid + " " + method + (ok ? " ok" : " error"));
|
||||
});
|
||||
if (agentServer->start(agentPort)) {
|
||||
logAgentEvent("Agent server started on port " + std::to_string(agentPort));
|
||||
@@ -767,14 +781,12 @@ struct EditorState {
|
||||
return out;
|
||||
}
|
||||
|
||||
json processAgentRequest(const json& request) {
|
||||
json processAgentRequest(const json& request, const std::string& sessionId) {
|
||||
json response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
|
||||
|
||||
std::string method = request.value("method", "");
|
||||
logAgentEvent("Agent RPC: " + method);
|
||||
|
||||
if (method == "getAST") {
|
||||
if (!active() || !isStructured()) {
|
||||
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
|
||||
@@ -794,6 +806,11 @@ struct EditorState {
|
||||
}
|
||||
|
||||
if (method == "applyMutation") {
|
||||
auto permIt = agentMutationPermissions.find(sessionId);
|
||||
if (permIt == agentMutationPermissions.end() || !permIt->second) {
|
||||
response["error"] = {{"code", -32030}, {"message", "Mutation not permitted"}};
|
||||
return response;
|
||||
}
|
||||
if (!active() || !isStructured()) {
|
||||
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
|
||||
return response;
|
||||
|
||||
@@ -36,9 +36,14 @@ struct AgentSession {
|
||||
};
|
||||
|
||||
// Callback types
|
||||
using JsonRpcHandler = std::function<json(const json& request)>;
|
||||
using JsonRpcHandler =
|
||||
std::function<json(const json& request, const std::string& sessionId)>;
|
||||
using SessionEventCallback =
|
||||
std::function<void(const AgentSession& session, const std::string& event)>;
|
||||
using RequestLogCallback =
|
||||
std::function<void(const std::string& sessionId,
|
||||
const json& request,
|
||||
const json& response)>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocketTransport — abstract transport interface
|
||||
@@ -119,6 +124,10 @@ public:
|
||||
sessionEventCallback_ = std::move(cb);
|
||||
}
|
||||
|
||||
void setRequestLogCallback(RequestLogCallback cb) {
|
||||
requestLogCallback_ = std::move(cb);
|
||||
}
|
||||
|
||||
// --- session queries ---
|
||||
|
||||
const AgentSession* getSession(const std::string& sessionId) const {
|
||||
@@ -188,6 +197,13 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
if (requestLogCallback_) {
|
||||
try {
|
||||
requestLogCallback_(sessionId, json::parse(message), response);
|
||||
} catch (...) {
|
||||
// Ignore logging parse errors
|
||||
}
|
||||
}
|
||||
transport_->sendMessage(sessionId, response.dump());
|
||||
}
|
||||
|
||||
@@ -252,7 +268,7 @@ private:
|
||||
}
|
||||
else if (requestHandler_) {
|
||||
// Delegate to the orchestrator's processRequest
|
||||
response = requestHandler_(request);
|
||||
response = requestHandler_(request, sessionId);
|
||||
// Ensure jsonrpc and id are set
|
||||
response["jsonrpc"] = "2.0";
|
||||
if (!response.contains("id"))
|
||||
@@ -287,6 +303,7 @@ private:
|
||||
std::map<std::string, AgentSession> sessions_;
|
||||
JsonRpcHandler requestHandler_;
|
||||
SessionEventCallback sessionEventCallback_;
|
||||
RequestLogCallback requestLogCallback_;
|
||||
int port_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -1656,8 +1656,27 @@ int main(int, char**) {
|
||||
for (const auto& s : state.agentServer->getActiveSessions()) {
|
||||
std::string label = s.sessionId;
|
||||
if (!s.agentName.empty()) label += " (" + s.agentName + ")";
|
||||
label += " - msgs: " + std::to_string(s.messageCount);
|
||||
ImGui::BulletText("%s", label.c_str());
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::SameLine(260.0f);
|
||||
bool canMutate = state.agentMutationPermissions[s.sessionId];
|
||||
std::string checkId = "Can mutate##" + s.sessionId;
|
||||
if (ImGui::Checkbox(checkId.c_str(), &canMutate)) {
|
||||
state.agentMutationPermissions[s.sessionId] = canMutate;
|
||||
}
|
||||
ImGui::SameLine(420.0f);
|
||||
ImGui::Text("Msgs: %d", s.messageCount);
|
||||
ImGui::SameLine(520.0f);
|
||||
ImGui::Text("Last: %llu", (unsigned long long)s.lastMessageAtMs);
|
||||
ImGui::SameLine(680.0f);
|
||||
std::string btnId = "Disconnect##" + s.sessionId;
|
||||
if (ImGui::Button(btnId.c_str())) {
|
||||
if (state.agentTransport) {
|
||||
state.agentTransport->simulateDisconnect(s.sessionId);
|
||||
} else {
|
||||
state.logAgentEvent("STUB: disconnect not supported for real transport");
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ int main() {
|
||||
MockWebSocketTransport* raw = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
|
||||
server.setRequestHandler([](const json& req) {
|
||||
server.setRequestHandler([](const json& req, const std::string&) {
|
||||
json res;
|
||||
res["result"] = {{"ok", true}, {"echo", req.value("method", "")}};
|
||||
return res;
|
||||
|
||||
41
editor/tests/step125_test.cpp
Normal file
41
editor/tests/step125_test.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
// Step 125 TDD Test: Request log callback
|
||||
#include "WebSocketServer.h"
|
||||
#include <iostream>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
if (cond) {
|
||||
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
MockWebSocketTransport* raw = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
|
||||
bool logCalled = false;
|
||||
std::string lastMethod;
|
||||
server.setRequestLogCallback([&](const std::string&,
|
||||
const json& req,
|
||||
const json&) {
|
||||
logCalled = true;
|
||||
lastMethod = req.value("method", "");
|
||||
});
|
||||
|
||||
server.start(9001);
|
||||
std::string sid = raw->simulateConnect();
|
||||
json msg = {{"jsonrpc","2.0"},{"id",1},{"method","ping"}};
|
||||
raw->simulateMessage(sid, msg.dump());
|
||||
expect(logCalled, "request log callback", passed, failed);
|
||||
expect(lastMethod == "ping", "method logged", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 125 Results: " << passed << " passed, " << failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user