// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title SmartClawsChannel
* @dev Kafka-like append-only log with circular buffering based on byte capacity.
* Writes can be disabled while preserving reads.
*/
contract SmartClawsChannel {
address public owner;
address public immutable registry;
uint256 public maxCapacityBytes;
uint256 public totalBytes;
uint256 public startOffset;
uint256 public currentOffset;
bool public writesEnabled = true;
mapping(uint256 => bytes) private messages;
mapping(uint256 => uint256) private messageSizes;
mapping(address => bool) private authorizedPublishers;
address[] private publishersList;
mapping(address => uint256) private publisherIndexPlusOne;
event MessagePublished(address indexed channel, uint256 indexed offset);
event ChannelOwnerChanged(address indexed previousOwner, address indexed newOwner);
event PublisherAuthorized(address indexed publisher);
event PublisherDeauthorized(address indexed publisher);
event ChannelWritesDisabled(address indexed channel);
modifier onlyOwner() {
require(msg.sender == owner, "SmartClaws: Only owner");
_;
}
modifier onlyOwnerOrRegistry() {
require(msg.sender == owner || msg.sender == registry, "SmartClaws: Only owner or registry");
_;
}
modifier onlyAuthorized() {
require(writesEnabled, "SmartClaws: Writes disabled");
require(msg.sender == owner || authorizedPublishers[msg.sender], "SmartClaws: Unauthorized");
_;
}
constructor(address _owner, uint256 _maxCapacityBytes, address _registry) {
require(_owner != address(0), "SmartClaws: Zero owner");
require(_registry != address(0), "SmartClaws: Zero registry");
require(_maxCapacityBytes > 0, "SmartClaws: Capacity must be positive");
owner = _owner;
registry = _registry;
maxCapacityBytes = _maxCapacityBytes;
}
/**
* @notice Permanently disables future writes while preserving reads.
*/
function disableWrites() external onlyOwnerOrRegistry {
if (!writesEnabled) return;
writesEnabled = false;
emit ChannelWritesDisabled(address(this));
}
/**
* @notice Appends a message to the channel. Prunes oldest messages if needed.
*/
function publishMessage(bytes calldata _payload) external onlyAuthorized {
uint256 pSize = _payload.length;
require(pSize <= maxCapacityBytes, "SmartClaws: Payload exceeds capacity");
while (totalBytes + pSize > maxCapacityBytes && startOffset < currentOffset) {
totalBytes -= messageSizes[startOffset];
delete messages[startOffset];
delete messageSizes[startOffset];
startOffset++;
}
uint256 offset = currentOffset;
messages[offset] = _payload;
messageSizes[offset] = pSize;
totalBytes += pSize;
currentOffset++;
emit MessagePublished(address(this), offset);
}
function changeOwner(address _newOwnerAddress) external onlyOwner {
require(_newOwnerAddress != address(0), "SmartClaws: Zero address");
address oldOwner = owner;
owner = _newOwnerAddress;
emit ChannelOwnerChanged(oldOwner, _newOwnerAddress);
}
function addAuthorizedPublisherToChannel(address _publisher) external onlyOwner {
require(_publisher != address(0), "SmartClaws: Zero address");
require(_publisher != owner, "SmartClaws: Owner already authorized");
require(!authorizedPublishers[_publisher], "SmartClaws: Already authorized");
authorizedPublishers[_publisher] = true;
publishersList.push(_publisher);
publisherIndexPlusOne[_publisher] = publishersList.length;
emit PublisherAuthorized(_publisher);
}
function removeAuthorizedPublisherFromChannel(address _publisher) external onlyOwner {
require(_publisher != owner, "SmartClaws: Cannot remove owner");
require(authorizedPublishers[_publisher], "SmartClaws: Not authorized");
authorizedPublishers[_publisher] = false;
uint256 idxPlusOne = publisherIndexPlusOne[_publisher];
assert(idxPlusOne != 0);
uint256 idx = idxPlusOne - 1;
uint256 lastIdx = publishersList.length - 1;
if (idx != lastIdx) {
address moved = publishersList[lastIdx];
publishersList[idx] = moved;
publisherIndexPlusOne[moved] = idx + 1;
}
publishersList.pop();
delete publisherIndexPlusOne[_publisher];
emit PublisherDeauthorized(_publisher);
}
// --- Read API ---
function readMessage(uint256 _offset) external view returns (bytes memory payload) {
if (_offset < startOffset && _offset < currentOffset) {
revert("Message Pruned");
}
if (_offset >= currentOffset) {
revert("Invalid Offset");
}
return messages[_offset];
}
function getLatestMessageOffset() external view returns (uint256) {
require(currentOffset > 0, "SmartClaws: Channel empty");
return currentOffset - 1;
}
function getOldestMessageOffset() external view returns (uint256) {
require(currentOffset > startOffset, "SmartClaws: Channel empty");
return startOffset;
}
function getMessageCount() external view returns (uint256) {
if (currentOffset <= startOffset) return 0;
return currentOffset - startOffset;
}
function isAuthorizedPublisher(address _publisher) external view returns (bool) {
return _publisher == owner || authorizedPublishers[_publisher];
}
function getAuthorizedPublisherAddresses() external view returns (address[] memory) {
return publishersList;
}
function getMaxCapacityBytes() external view returns (uint256) {
return maxCapacityBytes;
}
}
/**
* @title SmartClawsDevice
* @dev Contract representing an individual device.
*/
contract SmartClawsDevice {
address public immutable incomingChannel;
address public immutable outgoingChannel;
address public immutable publisher;
constructor(address _incoming, address _outgoing, address _publisher) {
require(_incoming != address(0), "SmartClaws: Zero incoming");
require(_outgoing != address(0), "SmartClaws: Zero outgoing");
require(_publisher != address(0), "SmartClaws: Zero publisher");
incomingChannel = _incoming;
outgoingChannel = _outgoing;
publisher = _publisher;
}
function getIncomingMessagesChannel() external view returns (address) {
return incomingChannel;
}
function getOutgoingMessagesChannel() external view returns (address) {
return outgoingChannel;
}
}
/**
* @title SmartClawsAgent
* @dev Contract representing an individual OpenClaw AI Agent.
* The outgoing channel is disabled by the registry when the agent is unregistered.
*/
contract SmartClawsAgent {
address public owner;
address public immutable registry;
address public immutable incomingChannel;
address public immutable outgoingChannel;
bool public active = true;
event AgentOwnerChanged(address indexed previousOwner, address indexed newOwner);
event AgentDeactivated(address indexed agent);
modifier onlyOwner() {
require(msg.sender == owner, "SmartClaws: Only owner");
_;
}
modifier onlyRegistry() {
require(msg.sender == registry, "SmartClaws: Only registry");
_;
}
constructor(address _owner, address _incoming, address _outgoing, address _registry) {
require(_owner != address(0), "SmartClaws: Zero owner");
require(_incoming != address(0), "SmartClaws: Zero incoming");
require(_outgoing != address(0), "SmartClaws: Zero outgoing");
require(_registry != address(0), "SmartClaws: Zero registry");
owner = _owner;
incomingChannel = _incoming;
outgoingChannel = _outgoing;
registry = _registry;
}
function changeOwner(address _newOwnerAddress) external onlyOwner {
require(_newOwnerAddress != address(0), "SmartClaws: Zero address");
// Keep channel ownership aligned with agent ownership.
SmartClawsChannel(incomingChannel).changeOwner(_newOwnerAddress);
SmartClawsChannel(outgoingChannel).changeOwner(_newOwnerAddress);
address oldOwner = owner;
owner = _newOwnerAddress;
emit AgentOwnerChanged(oldOwner, _newOwnerAddress);
}
function deactivate() external onlyRegistry {
if (!active) return;
active = false;
emit AgentDeactivated(address(this));
}
function getIncomingMessagesChannel() external view returns (address) {
return incomingChannel;
}
function getOutgoingMessagesChannel() external view returns (address) {
return outgoingChannel;
}
}
/**
* @title SmartClawsDeviceGroup
* @dev Manages registration of devices within a category.
* The group contract owns device channels so it can reliably revoke permissions.
*/
contract SmartClawsDeviceGroup {
address public owner;
address public immutable registry;
string public groupName;
string public skills;
bool public active = true;
struct DeviceInfo {
bool registered;
address publisher;
address incomingChannel;
address outgoingChannel;
}
mapping(address => DeviceInfo) public deviceInfo;
address[] public deviceList;
event DeviceRegistered(address indexed device, string deviceId);
event DeviceUnregistered(address indexed device);
event GroupOwnerChanged(address indexed previousOwner, address indexed newOwner);
event DeviceGroupDeactivated(address indexed group);
modifier onlyOwner() {
require(msg.sender == owner, "SmartClaws: Only owner");
_;
}
modifier onlyRegistry() {
require(msg.sender == registry, "SmartClaws: Only registry");
_;
}
constructor(address _owner, string memory _name, string memory _skills, address _registry) {
require(_owner != address(0), "SmartClaws: Zero owner");
require(_registry != address(0), "SmartClaws: Zero registry");
owner = _owner;
groupName = _name;
skills = _skills;
registry = _registry;
}
/**
* @notice ABI change required: a device needs a publisher address or unregister cannot revoke it.
*/
function registerDevice(
string calldata _deviceId,
address _devicePublisher
) external onlyOwner returns (address device) {
require(active, "SmartClaws: Group inactive");
require(_devicePublisher != address(0), "SmartClaws: Zero publisher");
// Group owns the channels so it can revoke permissions later.
SmartClawsChannel inc = new SmartClawsChannel(address(this), 1024 * 1024, registry);
SmartClawsChannel out = new SmartClawsChannel(address(this), 1024 * 1024, registry);
// Device publishes telemetry on outgoing channel.
out.addAuthorizedPublisherToChannel(_devicePublisher);
SmartClawsDevice newDev = new SmartClawsDevice(address(inc), address(out), _devicePublisher);
device = address(newDev);
deviceInfo[device] = DeviceInfo({
registered: true,
publisher: _devicePublisher,
incomingChannel: address(inc),
outgoingChannel: address(out)
});
deviceList.push(device);
emit DeviceRegistered(device, _deviceId);
}
function unregisterDevice(address _device) external onlyOwner {
DeviceInfo storage info = deviceInfo[_device];
require(info.registered, "SmartClaws: Device not registered");
// Spec: revoke publishing permissions from all associated channels.
SmartClawsChannel(info.outgoingChannel).removeAuthorizedPublisherFromChannel(info.publisher);
info.registered = false;
emit DeviceUnregistered(_device);
}
function deactivate() external onlyRegistry {
if (!active) return;
active = false;
emit DeviceGroupDeactivated(address(this));
}
function changeOwner(address _newOwnerAddress) external onlyOwner {
require(_newOwnerAddress != address(0), "SmartClaws: Zero address");
address oldOwner = owner;
owner = _newOwnerAddress;
emit GroupOwnerChanged(oldOwner, _newOwnerAddress);
}
}
/**
* @title SmartClaws
* @dev Global registry and entry point.
*/
contract SmartClaws {
address[] public allChannels;
address[] public allDeviceGroups;
address[] public allAgents;
mapping(address => bool) public registeredChannel;
mapping(address => bool) public registeredDeviceGroup;
mapping(address => bool) public registeredAgent;
event ChannelCreated(address indexed channel, address indexed owner);
event ChannelDeleted(address indexed channel);
event DeviceGroupRegistered(address indexed deviceGroup, string deviceGroupName);
event DeviceGroupUnregistered(address indexed deviceGroup);
event AgentRegistered(address indexed agent, string agentId);
event AgentUnregistered(address indexed agent);
function createChannel(address _ownerAddress, uint256 _maxCapacityBytes) external returns (address channel) {
SmartClawsChannel newChannel = new SmartClawsChannel(_ownerAddress, _maxCapacityBytes, address(this));
channel = address(newChannel);
allChannels.push(channel);
registeredChannel[channel] = true;
emit ChannelCreated(channel, _ownerAddress);
}
function deleteChannel(address _channelId) external {
require(registeredChannel[_channelId], "SmartClaws: Channel not registered");
SmartClawsChannel chan = SmartClawsChannel(_channelId);
require(msg.sender == chan.owner(), "SmartClaws: Only owner");
chan.disableWrites();
_removeFromArr(allChannels, _channelId);
registeredChannel[_channelId] = false;
emit ChannelDeleted(_channelId);
}
function registerDeviceGroup(
string calldata _deviceGroupName,
string calldata _skills
) external returns (address deviceGroup) {
SmartClawsDeviceGroup newGroup = new SmartClawsDeviceGroup(
msg.sender,
_deviceGroupName,
_skills,
address(this)
);
deviceGroup = address(newGroup);
allDeviceGroups.push(deviceGroup);
registeredDeviceGroup[deviceGroup] = true;
emit DeviceGroupRegistered(deviceGroup, _deviceGroupName);
}
function unregisterDeviceGroup(address _deviceGroup) external {
require(registeredDeviceGroup[_deviceGroup], "SmartClaws: Group not registered");
SmartClawsDeviceGroup group = SmartClawsDeviceGroup(_deviceGroup);
require(msg.sender == group.owner(), "SmartClaws: Only owner");
group.deactivate();
_removeFromArr(allDeviceGroups, _deviceGroup);
registeredDeviceGroup[_deviceGroup] = false;
emit DeviceGroupUnregistered(_deviceGroup);
}
function registerAgent(string calldata _agentId, string calldata /* _metadata */) external returns (address agent) {
SmartClawsChannel inc = new SmartClawsChannel(msg.sender, 1024 * 1024, address(this));
SmartClawsChannel out = new SmartClawsChannel(msg.sender, 1024 * 1024, address(this));
SmartClawsAgent newAgent = new SmartClawsAgent(
msg.sender,
address(inc),
address(out),
address(this)
);
agent = address(newAgent);
allAgents.push(agent);
registeredAgent[agent] = true;
emit AgentRegistered(agent, _agentId);
}
function unregisterAgent(address _agent) external {
require(registeredAgent[_agent], "SmartClaws: Agent not registered");
SmartClawsAgent agentContract = SmartClawsAgent(_agent);
require(msg.sender == agentContract.owner(), "SmartClaws: Only owner");
// Spec: disable future publishing, preserve reads.
SmartClawsChannel(agentContract.getOutgoingMessagesChannel()).disableWrites();
agentContract.deactivate();
_removeFromArr(allAgents, _agent);
registeredAgent[_agent] = false;
emit AgentUnregistered(_agent);
}
function _removeFromArr(address[] storage arr, address target) internal {
uint256 len = arr.length;
for (uint256 i = 0; i < len; i++) {
if (arr[i] == target) {
arr[i] = arr[len - 1];
arr.pop();
return;
}
}
revert("SmartClaws: Target not found");
}
}