implement Milestone 18: inter-app MessageBus with intent system

This commit is contained in:
2026-01-18 16:36:08 +01:00
parent 72a06f542b
commit 372a293bd0
6 changed files with 1310 additions and 1 deletions

View File

@@ -0,0 +1,546 @@
// message_bus.cpp - Inter-app message bus implementation
// Milestone 18: Kernel-mediated message passing between apps
#include "message_bus.h"
#include <lua.hpp>
#include <algorithm>
namespace mosis {
// ============================================================================
// MessageBus
// ============================================================================
MessageBus::MessageBus() = default;
MessageBus::~MessageBus() = default;
bool MessageBus::MatchesFilter(const Intent& intent, const IntentFilter& filter) const {
// Action must match
if (filter.action != intent.action) {
return false;
}
// If no types specified, accept all
if (filter.types.empty()) {
return true;
}
// Check if intent type matches any filter type
for (const auto& t : filter.types) {
if (t == intent.type) {
return true;
}
}
return false;
}
bool MessageBus::ValidateIntent(const Intent& intent, std::string& error) const {
if (intent.action.empty()) {
error = "intent action is required";
return false;
}
// Note: Data size is checked separately to return DataTooLarge error
return true;
}
void MessageBus::RegisterReceiver(
const std::string& app_id,
const IntentFilter& filter,
IntentHandler handler
) {
std::lock_guard<std::mutex> lock(m_mutex);
m_receivers.push_back({app_id, filter, std::move(handler)});
}
void MessageBus::UnregisterApp(const std::string& app_id) {
std::lock_guard<std::mutex> lock(m_mutex);
m_receivers.erase(
std::remove_if(m_receivers.begin(), m_receivers.end(),
[&app_id](const ReceiverEntry& e) { return e.app_id == app_id; }),
m_receivers.end()
);
}
MessageError MessageBus::Send(
const std::string& sender_app,
const std::string& target_app,
const Intent& intent,
std::string& error
) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check if sender is blocked
if (m_blocked_apps.count(sender_app)) {
error = "sender app is blocked";
return MessageError::SenderBlocked;
}
// Validate intent
if (!ValidateIntent(intent, error)) {
return MessageError::InvalidIntent;
}
// Check data size
if (intent.data.size() > MAX_DATA_SIZE) {
error = "intent data exceeds maximum size";
return MessageError::DataTooLarge;
}
// Find target receiver
bool found = false;
for (const auto& entry : m_receivers) {
if (entry.app_id == target_app && MatchesFilter(intent, entry.filter)) {
// Create intent with sender info
Intent deliveredIntent = intent;
deliveredIntent.from_app = sender_app;
// Deliver
entry.handler(deliveredIntent);
found = true;
m_message_count++;
break;
}
}
if (!found) {
error = "no receiver found for target app with matching action";
return MessageError::ReceiverNotFound;
}
return MessageError::None;
}
MessageError MessageBus::Broadcast(
const std::string& sender_app,
const Intent& intent,
std::string& error
) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check if sender is blocked
if (m_blocked_apps.count(sender_app)) {
error = "sender app is blocked";
return MessageError::SenderBlocked;
}
// Validate intent
if (!ValidateIntent(intent, error)) {
return MessageError::InvalidIntent;
}
// Check data size
if (intent.data.size() > MAX_DATA_SIZE) {
error = "intent data exceeds maximum size";
return MessageError::DataTooLarge;
}
// Collect matching handlers (avoid holding lock during callbacks)
std::vector<IntentHandler> handlers;
for (const auto& entry : m_receivers) {
// Skip blocked receivers
if (m_blocked_apps.count(entry.app_id)) {
continue;
}
if (MatchesFilter(intent, entry.filter)) {
handlers.push_back(entry.handler);
}
}
if (handlers.empty()) {
error = "no receivers registered for action: " + intent.action;
return MessageError::NoReceivers;
}
// Create intent with sender info
Intent deliveredIntent = intent;
deliveredIntent.from_app = sender_app;
// Deliver to all matching handlers
for (const auto& handler : handlers) {
handler(deliveredIntent);
m_message_count++;
}
return MessageError::None;
}
bool MessageBus::HasReceiverFor(const std::string& action) const {
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& entry : m_receivers) {
if (entry.filter.action == action && !m_blocked_apps.count(entry.app_id)) {
return true;
}
}
return false;
}
std::vector<std::string> MessageBus::GetReceiversFor(const std::string& action) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<std::string> result;
std::unordered_set<std::string> seen;
for (const auto& entry : m_receivers) {
if (entry.filter.action == action &&
!m_blocked_apps.count(entry.app_id) &&
!seen.count(entry.app_id)) {
result.push_back(entry.app_id);
seen.insert(entry.app_id);
}
}
return result;
}
void MessageBus::SetAppPermission(const std::string& app_id, const std::string& permission, bool granted) {
std::lock_guard<std::mutex> lock(m_mutex);
if (granted) {
m_permissions[app_id].insert(permission);
} else {
auto it = m_permissions.find(app_id);
if (it != m_permissions.end()) {
it->second.erase(permission);
}
}
}
bool MessageBus::HasPermission(const std::string& app_id, const std::string& permission) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_permissions.find(app_id);
if (it == m_permissions.end()) {
return false;
}
return it->second.count(permission) > 0;
}
void MessageBus::BlockApp(const std::string& app_id) {
std::lock_guard<std::mutex> lock(m_mutex);
m_blocked_apps.insert(app_id);
}
void MessageBus::UnblockApp(const std::string& app_id) {
std::lock_guard<std::mutex> lock(m_mutex);
m_blocked_apps.erase(app_id);
}
bool MessageBus::IsBlocked(const std::string& app_id) const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_blocked_apps.count(app_id) > 0;
}
void MessageBus::Clear() {
std::lock_guard<std::mutex> lock(m_mutex);
m_receivers.clear();
m_permissions.clear();
m_blocked_apps.clear();
m_message_count = 0;
}
size_t MessageBus::GetReceiverCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_receivers.size();
}
// ============================================================================
// Lua API
// ============================================================================
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global (at -1)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if _G has a metatable with __index (sandbox proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found proxy, set in the real _G (__index table)
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name);
lua_pop(L, 4); // Pop __index, metatable, _G, original value
return;
}
lua_pop(L, 2); // Pop nil/__index and metatable
}
// No proxy, set in _G directly
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name);
lua_pop(L, 2); // Pop _G and original value
}
static const char* MESSAGE_BUS_KEY = "mosis.message_bus";
static const char* INTENTS_APP_ID_KEY = "mosis.intents_app_id";
static MessageBus* GetMessageBus(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, MESSAGE_BUS_KEY);
auto* bus = static_cast<MessageBus*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return bus;
}
static std::string GetAppId(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, INTENTS_APP_ID_KEY);
std::string app_id;
if (lua_isstring(L, -1)) {
app_id = lua_tostring(L, -1);
}
lua_pop(L, 1);
return app_id;
}
// Read Intent from Lua table
static Intent ReadIntent(lua_State* L, int index) {
Intent intent;
if (lua_type(L, index) != LUA_TTABLE) {
return intent;
}
lua_getfield(L, index, "action");
if (lua_isstring(L, -1)) {
intent.action = lua_tostring(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, index, "type");
if (lua_isstring(L, -1)) {
intent.type = lua_tostring(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, index, "data");
if (lua_isstring(L, -1)) {
intent.data = lua_tostring(L, -1);
}
lua_pop(L, 1);
return intent;
}
// Push Intent as Lua table
static void PushIntent(lua_State* L, const Intent& intent) {
lua_createtable(L, 0, 4);
lua_pushstring(L, intent.action.c_str());
lua_setfield(L, -2, "action");
lua_pushstring(L, intent.type.c_str());
lua_setfield(L, -2, "type");
lua_pushstring(L, intent.data.c_str());
lua_setfield(L, -2, "data");
lua_pushstring(L, intent.from_app.c_str());
lua_setfield(L, -2, "from");
}
// intents.on(action, handler, [options])
static int lua_intents_on(lua_State* L) {
auto* bus = GetMessageBus(L);
if (!bus) {
return luaL_error(L, "message bus not available");
}
std::string app_id = GetAppId(L);
if (app_id.empty()) {
return luaL_error(L, "app id not set");
}
const char* action = luaL_checkstring(L, 1);
luaL_checktype(L, 2, LUA_TFUNCTION);
IntentFilter filter;
filter.action = action;
// Parse optional options table
if (lua_gettop(L) >= 3 && lua_istable(L, 3)) {
lua_getfield(L, 3, "types");
if (lua_istable(L, -1)) {
int len = static_cast<int>(lua_rawlen(L, -1));
for (int i = 1; i <= len; i++) {
lua_rawgeti(L, -1, i);
if (lua_isstring(L, -1)) {
filter.types.push_back(lua_tostring(L, -1));
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
}
// Store handler reference
lua_pushvalue(L, 2); // Copy handler function
int handler_ref = luaL_ref(L, LUA_REGISTRYINDEX);
// Register receiver with C++ callback that invokes Lua handler
bus->RegisterReceiver(app_id, filter, [L, handler_ref](const Intent& intent) {
// Get handler function
lua_rawgeti(L, LUA_REGISTRYINDEX, handler_ref);
// Push intent as argument
PushIntent(L, intent);
// Call handler(intent)
if (lua_pcall(L, 1, 0, 0) != LUA_OK) {
// Log error but don't propagate
lua_pop(L, 1);
}
});
return 0;
}
// intents.send(target_app, intent) -> success, error
static int lua_intents_send(lua_State* L) {
auto* bus = GetMessageBus(L);
if (!bus) {
return luaL_error(L, "message bus not available");
}
std::string app_id = GetAppId(L);
if (app_id.empty()) {
return luaL_error(L, "app id not set");
}
const char* target = luaL_checkstring(L, 1);
luaL_checktype(L, 2, LUA_TTABLE);
Intent intent = ReadIntent(L, 2);
std::string error;
auto result = bus->Send(app_id, target, intent, error);
if (result == MessageError::None) {
lua_pushboolean(L, 1);
lua_pushnil(L);
} else {
lua_pushboolean(L, 0);
lua_pushstring(L, error.c_str());
}
return 2;
}
// intents.broadcast(intent) -> count
static int lua_intents_broadcast(lua_State* L) {
auto* bus = GetMessageBus(L);
if (!bus) {
return luaL_error(L, "message bus not available");
}
std::string app_id = GetAppId(L);
if (app_id.empty()) {
return luaL_error(L, "app id not set");
}
luaL_checktype(L, 1, LUA_TTABLE);
Intent intent = ReadIntent(L, 1);
// Get receiver count before broadcast
auto receivers = bus->GetReceiversFor(intent.action);
std::string error;
auto result = bus->Broadcast(app_id, intent, error);
if (result == MessageError::None) {
lua_pushinteger(L, static_cast<int>(receivers.size()));
} else if (result == MessageError::NoReceivers) {
lua_pushinteger(L, 0);
} else {
return luaL_error(L, "%s", error.c_str());
}
return 1;
}
// intents.hasReceiver(action) -> boolean
static int lua_intents_hasReceiver(lua_State* L) {
auto* bus = GetMessageBus(L);
if (!bus) {
return luaL_error(L, "message bus not available");
}
const char* action = luaL_checkstring(L, 1);
lua_pushboolean(L, bus->HasReceiverFor(action));
return 1;
}
// intents.getReceivers(action) -> array of app IDs
static int lua_intents_getReceivers(lua_State* L) {
auto* bus = GetMessageBus(L);
if (!bus) {
return luaL_error(L, "message bus not available");
}
const char* action = luaL_checkstring(L, 1);
auto receivers = bus->GetReceiversFor(action);
lua_createtable(L, static_cast<int>(receivers.size()), 0);
for (size_t i = 0; i < receivers.size(); i++) {
lua_pushstring(L, receivers[i].c_str());
lua_rawseti(L, -2, static_cast<int>(i + 1));
}
return 1;
}
// intents.unregisterAll()
static int lua_intents_unregisterAll(lua_State* L) {
auto* bus = GetMessageBus(L);
if (!bus) {
return luaL_error(L, "message bus not available");
}
std::string app_id = GetAppId(L);
if (!app_id.empty()) {
bus->UnregisterApp(app_id);
}
return 0;
}
void RegisterIntentsAPI(lua_State* L, MessageBus* bus, const std::string& app_id) {
// Store bus pointer
lua_pushlightuserdata(L, bus);
lua_setfield(L, LUA_REGISTRYINDEX, MESSAGE_BUS_KEY);
// Store app ID
lua_pushstring(L, app_id.c_str());
lua_setfield(L, LUA_REGISTRYINDEX, INTENTS_APP_ID_KEY);
// Create intents table
lua_createtable(L, 0, 6);
lua_pushcfunction(L, lua_intents_on);
lua_setfield(L, -2, "on");
lua_pushcfunction(L, lua_intents_send);
lua_setfield(L, -2, "send");
lua_pushcfunction(L, lua_intents_broadcast);
lua_setfield(L, -2, "broadcast");
lua_pushcfunction(L, lua_intents_hasReceiver);
lua_setfield(L, -2, "hasReceiver");
lua_pushcfunction(L, lua_intents_getReceivers);
lua_setfield(L, -2, "getReceivers");
lua_pushcfunction(L, lua_intents_unregisterAll);
lua_setfield(L, -2, "unregisterAll");
// Set as global "intents" (bypassing sandbox proxy)
SetGlobalInRealG(L, "intents");
}
} // namespace mosis

View File

@@ -0,0 +1,114 @@
// message_bus.h - Inter-app message bus for Lua sandbox
// Milestone 18: Kernel-mediated message passing between apps
#pragma once
#include <string>
#include <vector>
#include <functional>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
struct lua_State;
namespace mosis {
struct Intent {
std::string action; // Intent action (e.g., "share", "view", "edit")
std::string type; // MIME type (e.g., "text/plain", "image/png")
std::string data; // Intent data/payload
std::string from_app; // Sender app ID (set by kernel)
};
struct IntentFilter {
std::string action; // Required action
std::vector<std::string> types; // Accepted MIME types (empty = all)
};
enum class MessageError {
None,
NoReceivers,
PermissionDenied,
InvalidIntent,
DataTooLarge,
ReceiverNotFound,
SenderBlocked
};
class MessageBus {
public:
using IntentHandler = std::function<void(const Intent&)>;
MessageBus();
~MessageBus();
// Register an app to receive intents
void RegisterReceiver(
const std::string& app_id,
const IntentFilter& filter,
IntentHandler handler
);
// Unregister all handlers for an app
void UnregisterApp(const std::string& app_id);
// Send intent to specific app (requires permission)
MessageError Send(
const std::string& sender_app,
const std::string& target_app,
const Intent& intent,
std::string& error
);
// Broadcast intent to all registered receivers
MessageError Broadcast(
const std::string& sender_app,
const Intent& intent,
std::string& error
);
// Check if any app handles this action
bool HasReceiverFor(const std::string& action) const;
// Get list of apps that handle an action
std::vector<std::string> GetReceiversFor(const std::string& action) const;
// Permission management
void SetAppPermission(const std::string& app_id, const std::string& permission, bool granted);
bool HasPermission(const std::string& app_id, const std::string& permission) const;
// Block specific app from sending/receiving
void BlockApp(const std::string& app_id);
void UnblockApp(const std::string& app_id);
bool IsBlocked(const std::string& app_id) const;
// Clear all registrations (for testing)
void Clear();
// Statistics
size_t GetReceiverCount() const;
size_t GetMessageCount() const { return m_message_count; }
private:
struct ReceiverEntry {
std::string app_id;
IntentFilter filter;
IntentHandler handler;
};
std::vector<ReceiverEntry> m_receivers;
std::unordered_map<std::string, std::unordered_set<std::string>> m_permissions;
std::unordered_set<std::string> m_blocked_apps;
mutable std::mutex m_mutex;
size_t m_message_count = 0;
static constexpr size_t MAX_DATA_SIZE = 1024 * 1024; // 1MB max
bool MatchesFilter(const Intent& intent, const IntentFilter& filter) const;
bool ValidateIntent(const Intent& intent, std::string& error) const;
};
// Register intents.* APIs as globals
void RegisterIntentsAPI(lua_State* L, MessageBus* bus, const std::string& app_id);
} // namespace mosis