SmartClaws Reference Implementation

// 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");
    }
}