security: comprehensive pre-PR security audit fixes (rebased)

This commit is contained in:
ronitchidara 2026-01-28 13:48:29 +05:30
parent 01b5a757d9
commit 5aae2aa197
83 changed files with 798 additions and 304 deletions

View File

@ -1,8 +1,8 @@
---
description: Update Clawdbot from upstream when branch has diverged (ahead/behind)
description: Update Moltbot from upstream when branch has diverged (ahead/behind)
---
# Clawdbot Upstream Sync Workflow
# Moltbot Upstream Sync Workflow
Use this workflow when your fork has diverged from upstream (e.g., "18 commits ahead, 29 commits behind").
@ -129,10 +129,10 @@ pnpm mac:package
```bash
# Kill running app
pkill -x "Clawdbot" || true
pkill -x "Moltbot" || true
# Move old version
mv /Applications/Clawdbot.app /tmp/Clawdbot-backup.app
mv /Applications/Clawdbot.app /tmp/Moltbot-backup.app
# Install new build
cp -R dist/Clawdbot.app /Applications/

View File

@ -1,6 +1,6 @@
---
name: Bug report
about: Report a problem or unexpected behavior in Clawdbot.
about: Report a problem or unexpected behavior in Moltbot.
title: "[Bug]: "
labels: bug
---
@ -20,7 +20,7 @@ What did you expect to happen?
What actually happened?
## Environment
- Clawdbot version:
- Moltbot version:
- OS:
- Install method (pnpm/npx/docker/etc):

View File

@ -2,7 +2,7 @@ blank_issues_enabled: true
contact_links:
- name: Onboarding
url: https://discord.gg/clawd
about: New to Clawdbot? Join Discord for setup guidance from Krill in #help.
about: New to Moltbot? Join Discord for setup guidance from Krill in #help.
- name: Support
url: https://discord.gg/clawd
about: Get help from Krill and the community on Discord in #help.

View File

@ -1,6 +1,6 @@
---
name: Feature request
about: Suggest an idea or improvement for Clawdbot.
about: Suggest an idea or improvement for Moltbot.
title: "[Feature]: "
labels: enhancement
---
@ -9,7 +9,7 @@ labels: enhancement
Describe the problem you are trying to solve or the opportunity you see.
## Proposed solution
What would you like Clawdbot to do?
What would you like Moltbot to do?
## Alternatives considered
Any other approaches you have considered?

View File

@ -43,7 +43,7 @@ AI PRs are first-class citizens here. We just want transparency so reviewers kno
## Contribution Guides
Detailed guides for extending Clawdbot:
Detailed guides for extending Moltbot:
- **[Skills Guide](https://docs.clawd.bot/contributing/skills)** - Create and publish skills to ClawdHub
- **[Plugins Guide](https://docs.clawd.bot/contributing/plugins)** - Build plugins for channels, tools, and more

View File

@ -1,4 +1,4 @@
## Clawdbot Node (Android) (internal)
## Moltbot Node (Android) (internal)
Modern Android node app: connects to the **Gateway WebSocket** (`_clawdbot-gw._tcp`) and exposes **Canvas + Chat + Camera**.

View File

@ -316,11 +316,28 @@ internal fun NodeRuntime.hasBackgroundLocationPermission(): Boolean {
// MARK: - Helper Extension
/**
* Escape a string for safe embedding in JSON and JavaScript.
*
* SECURITY: Handles all necessary escaping including:
* - Standard JSON escapes (backslash, quotes, control chars)
* - HTML script-breaking sequences (defense-in-depth)
* - Unicode control characters
*/
private fun String.toJsonStringEscaped(): String {
val escaped = this
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
.replace("\b", "\\b")
.replace("\u000C", "\\f")
// Defense-in-depth: escape sequences that could break HTML script context
.replace("</script>", "<\\/script>", ignoreCase = true)
.replace("<!--", "<\\!--")
// Escape Unicode line/paragraph separators (valid JSON but can break JS)
.replace("\u2028", "\\u2028")
.replace("\u2029", "\\u2029")
return "\"$escaped\""
}

View File

@ -170,6 +170,12 @@ internal fun NodeRuntime.decodeA2uiMessages(command: String, paramsJson: String?
return JsonArray(out).toString()
}
/**
* Validate A2UI v0.8 message structure.
*
* SECURITY: Performs deep validation of message structure to prevent
* malicious payloads that might pass shallow key-only checks.
*/
private fun validateA2uiV0_8(msg: JsonObject, lineNumber: Int) {
if (msg.containsKey("createSurface")) {
throw IllegalArgumentException(
@ -184,6 +190,43 @@ private fun validateA2uiV0_8(msg: JsonObject, lineNumber: Int) {
"A2UI JSONL line $lineNumber: expected exactly one of ${allowed.sorted().joinToString(", ")}; found: $found",
)
}
// Deep validation: ensure the command payload is properly structured
val commandKey = matched.first()
val payload = msg[commandKey]
when (commandKey) {
"beginRendering" -> {
// beginRendering should be an object with optional fields
if (payload != null && payload !is JsonObject && payload !is kotlinx.serialization.json.JsonNull) {
throw IllegalArgumentException(
"A2UI JSONL line $lineNumber: beginRendering value must be an object or null",
)
}
}
"surfaceUpdate", "dataModelUpdate" -> {
// These should be objects
if (payload !is JsonObject) {
throw IllegalArgumentException(
"A2UI JSONL line $lineNumber: $commandKey value must be an object",
)
}
}
"deleteSurface" -> {
// deleteSurface should contain a surfaceId string
if (payload !is JsonObject) {
throw IllegalArgumentException(
"A2UI JSONL line $lineNumber: deleteSurface value must be an object",
)
}
val surfaceId = (payload["surfaceId"] as? JsonPrimitive)?.contentOrNull
if (surfaceId.isNullOrBlank()) {
throw IllegalArgumentException(
"A2UI JSONL line $lineNumber: deleteSurface.surfaceId must be a non-empty string",
)
}
}
}
}
// MARK: - Helper Methods

View File

@ -45,7 +45,7 @@ class ClawdbotMessagingService : FirebaseMessagingService() {
message.notification?.let { notification ->
Log.d(TAG, "FCM notification payload: title=${notification.title}, body=${notification.body}")
showNotification(
title = notification.title ?: "Clawdbot",
title = notification.title ?: "Moltbot",
body = notification.body ?: "New message"
)
}
@ -59,7 +59,7 @@ class ClawdbotMessagingService : FirebaseMessagingService() {
private fun handleDataMessage(data: Map<String, String>) {
val messageType = data["type"]
val title = data["title"] ?: "Clawdbot"
val title = data["title"] ?: "Moltbot"
val body = data["body"] ?: "New message"
val sessionKey = data["sessionKey"]
@ -133,27 +133,49 @@ class ClawdbotMessagingService : FirebaseMessagingService() {
}
/**
* Simple storage for FCM push token.
* Secure storage for FCM push token using EncryptedSharedPreferences.
* The token should be sent to the gateway when connection is established.
*
* SECURITY: Uses EncryptedSharedPreferences to protect tokens at rest.
* Falls back to regular SharedPreferences only if encryption fails (rare edge cases).
*/
object PushTokenStore {
private const val PREFS_NAME = "clawdbot_push"
private const val PREFS_NAME = "clawdbot_push_secure"
private const val KEY_FCM_TOKEN = "fcm_token"
private const val TAG = "PushTokenStore"
private fun getSecurePrefs(context: Context): android.content.SharedPreferences {
return try {
androidx.security.crypto.EncryptedSharedPreferences.create(
context,
PREFS_NAME,
androidx.security.crypto.MasterKeys.getOrCreate(
androidx.security.crypto.MasterKeys.AES256_GCM_SPEC
),
androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (e: Exception) {
// Fallback to regular prefs if encryption fails (shouldn't happen)
Log.e(TAG, "Failed to create encrypted prefs, falling back to regular: ${e.message}")
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
}
fun saveToken(context: Context, token: String) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
getSecurePrefs(context)
.edit()
.putString(KEY_FCM_TOKEN, token)
.apply()
}
fun getToken(context: Context): String? {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return getSecurePrefs(context)
.getString(KEY_FCM_TOKEN, null)
}
fun clearToken(context: Context) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
getSecurePrefs(context)
.edit()
.remove(KEY_FCM_TOKEN)
.apply()

View File

@ -334,7 +334,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
style = MaterialTheme.typography.titleMedium,
)
Text(
"Make sure your Clawdbot gateway is running on the same network.",
"Make sure your Moltbot gateway is running on the same network.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,

View File

@ -143,7 +143,7 @@ fun ChatComposer(
value = input,
onValueChange = { input = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Message Clawd") },
placeholder = { Text("Message Molty") },
minLines = 2,
maxLines = 6,
)

View File

@ -103,7 +103,7 @@ private fun EmptyChatHint(modifier: Modifier = Modifier) {
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = "Message Clawd",
text = "Message Molty",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)

View File

@ -1,4 +1,4 @@
# Clawdbot (iOS)
# Moltbot (iOS)
Internal-only SwiftUI app scaffold.

View File

@ -84,7 +84,14 @@ final class OfflineMessageQueue {
private func save() {
do {
let data = try JSONEncoder().encode(pending)
try data.write(to: fileURL, options: .atomic)
// SECURITY: Use atomic write with data protection to encrypt at rest
try data.write(to: fileURL, options: [.atomic, .completeFileProtection])
// Ensure file protection is set (belt and suspenders)
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.complete],
ofItemAtPath: fileURL.path
)
} catch {
logger.error("Failed to save offline queue: \(error.localizedDescription)")
}

View File

@ -287,7 +287,7 @@ struct SettingsTab: View {
ContentUnavailableView {
Label("No Gateways Found", systemImage: "network.slash")
} description: {
Text("Make sure your Clawdbot gateway is running on the same network.")
Text("Make sure your Moltbot gateway is running on the same network.")
}
.frame(maxWidth: .infinity)
.padding(.vertical, 8)

View File

@ -107,7 +107,7 @@ struct TTSVoiceSettingsView: View {
private func testVoice() {
self.isTestPlaying = true
let utterance = AVSpeechUtterance(string: "Hello, I'm Clawdbot. How can I help you today?")
let utterance = AVSpeechUtterance(string: "Hello, I'm Moltbot. How can I help you today?")
// Apply selected voice
if !self.selectedVoiceId.isEmpty,

View File

@ -110,7 +110,7 @@ struct AgentWorkspaceTests {
try """
# IDENTITY.md - Agent Identity
- Name: Clawd
- Name: Molty
- Creature: Space Lobster
- Vibe: Helpful
- Emoji: lobster

View File

@ -0,0 +1,42 @@
{
"originHash" : "b4458983a5198aee04266fc147b2e91d455549a1f938db8f6476129de6338e0a",
"pins" : [
{
"identity" : "elevenlabskit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/ElevenLabsKit",
"state" : {
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
"version" : "0.1.0"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
"version" : "1.3.2"
}
},
{
"identity" : "swiftui-math",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swiftui-math",
"state" : {
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
"version" : "0.1.0"
}
},
{
"identity" : "textual",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/textual",
"state" : {
"revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38",
"version" : "0.3.1"
}
}
],
"version" : 3
}

View File

@ -230,7 +230,7 @@ struct MoltbotChatComposer: View {
private var editorOverlay: some View {
ZStack(alignment: .topLeading) {
if self.viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text("Message Clawd")
Text("Message Molty")
.foregroundStyle(.tertiary)
.padding(.horizontal, 4)
.padding(.vertical, 4)

View File

@ -459,7 +459,7 @@ struct ChatTypingIndicatorBubble: View {
HStack(spacing: 10) {
TypingDots()
if self.style == .standard {
Text("Clawd is thinking…")
Text("Molty is thinking…")
.font(.subheadline)
.foregroundStyle(.secondary)
Spacer()

View File

@ -69,13 +69,24 @@ public enum MoltbotCanvasA2UIJSONL: Sendable {
return items.map(\.message)
}
/// Encode messages to a JSON array string safe for embedding in JavaScript.
///
/// SECURITY: The output is JSON-encoded by JSONEncoder which handles escaping.
/// We additionally escape any `</script>` sequences to prevent XSS if the
/// JavaScript is embedded in HTML (defense-in-depth).
public static func encodeMessagesJSONArray(_ messages: [AnyCodable]) throws -> String {
let data = try JSONEncoder().encode(messages)
guard let json = String(data: data, encoding: .utf8) else {
guard var json = String(data: data, encoding: .utf8) else {
throw NSError(domain: "A2UI", code: 10, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode messages payload as UTF-8",
])
}
// Defense-in-depth: escape sequences that could break HTML script context
// JSONEncoder handles standard escaping but </script> can still appear in strings
json = json.replacingOccurrences(of: "</script>", with: "<\\/script>", options: [.caseInsensitive])
json = json.replacingOccurrences(of: "<!--", with: "<\\!--")
return json
}
}

View File

@ -1,10 +1,10 @@
# Clawdbot Chrome Extension (Browser Relay)
# Moltbot Chrome Extension (Browser Relay)
Purpose: attach Clawdbot to an existing Chrome tab so the Gateway can automate it (via the local CDP relay server).
Purpose: attach Moltbot to an existing Chrome tab so the Gateway can automate it (via the local CDP relay server).
## Dev / load unpacked
1. Build/run Clawdbot Gateway with browser control enabled.
1. Build/run Moltbot Gateway with browser control enabled.
2. Ensure the relay server is reachable at `http://127.0.0.1:18792/` (default).
3. Install the extension to a stable path:

View File

@ -1,16 +1,16 @@
---
title: Access Control Policy
summary: User access management, RBAC governance, and authorization procedures for Clawdbot deployments.
summary: User access management, RBAC governance, and authorization procedures for Moltbot deployments.
permalink: /compliance/access-control-policy/
---
# Access Control Policy
This policy defines the principles, roles, and procedures for managing user access to Clawdbot systems.
This policy defines the principles, roles, and procedures for managing user access to Moltbot systems.
## Policy Statement
Access to Clawdbot functionality shall be granted based on the principle of least privilege. Users receive only the permissions necessary to perform their authorized functions, and access is regularly reviewed and revoked when no longer needed.
Access to Moltbot functionality shall be granted based on the principle of least privilege. Users receive only the permissions necessary to perform their authorized functions, and access is regularly reviewed and revoked when no longer needed.
## Scope
@ -23,7 +23,7 @@ This policy applies to:
## Access Control Model
Clawdbot implements Role-Based Access Control (RBAC) as defined in `src/security/rbac.ts`.
Moltbot implements Role-Based Access Control (RBAC) as defined in `src/security/rbac.ts`.
### Built-in Roles

View File

@ -1,16 +1,16 @@
---
title: Change Management Procedure
summary: Procedures for managing configuration, version, and operational changes in Clawdbot deployments.
summary: Procedures for managing configuration, version, and operational changes in Moltbot deployments.
permalink: /compliance/change-management/
---
# Change Management Procedure
This document defines procedures for managing changes to Clawdbot configuration, versions, channels, and plugins in a controlled manner.
This document defines procedures for managing changes to Moltbot configuration, versions, channels, and plugins in a controlled manner.
## Purpose
Ensure all changes to Clawdbot systems are:
Ensure all changes to Moltbot systems are:
- Authorized before implementation
- Tested appropriately
- Documented with audit trail
@ -20,7 +20,7 @@ Ensure all changes to Clawdbot systems are:
This procedure covers:
- Configuration changes (settings, credentials, policies)
- Version upgrades (Clawdbot releases)
- Version upgrades (Moltbot releases)
- Channel modifications (adding, removing, reconfiguring)
- Plugin management (installation, updates, removal)
- Infrastructure changes (gateway, networking)
@ -176,7 +176,7 @@ clawdbot gateway stop
# Backup state directory
cp -r ~/.clawdbot ~/.clawdbot-backup-$(date +%Y%m%d)
# Update Clawdbot
# Update Moltbot
clawdbot update
# or: npm update -g clawdbot

View File

@ -1,17 +1,17 @@
---
title: Incident Response Procedure
summary: Security incident detection, classification, response, and recovery procedures for Clawdbot deployments.
summary: Security incident detection, classification, response, and recovery procedures for Moltbot deployments.
permalink: /compliance/incident-response/
---
# Incident Response Procedure
This document defines the procedures for detecting, classifying, responding to, and recovering from security incidents in Clawdbot deployments.
This document defines the procedures for detecting, classifying, responding to, and recovering from security incidents in Moltbot deployments.
## Scope
This procedure applies to:
- Unauthorized access attempts to the Clawdbot gateway
- Unauthorized access attempts to the Moltbot gateway
- Compromise of messaging channel credentials
- Malicious command execution via connected channels
- Data exfiltration or unauthorized data access
@ -238,7 +238,7 @@ clawdbot config set security.promptInjection.mode "strict"
- Reset modified configurations
4. **Patch vulnerabilities**
- Update Clawdbot if applicable: `clawdbot update`
- Update Moltbot if applicable: `clawdbot update`
- Apply configuration hardening per security audit recommendations
### Phase 4: Recovery (P1: 4 hr, P2: 48 hr, P3: 1 week)
@ -350,7 +350,7 @@ Conduct review within 5 business days of incident closure.
```
Subject: [SECURITY INCIDENT] P[N] - [Brief description]
A security incident has been detected in the Clawdbot deployment.
A security incident has been detected in the Moltbot deployment.
Severity: P[N]
Status: [Triage/Containment/Eradication/Recovery]

View File

@ -1,16 +1,16 @@
---
title: Compliance Overview
summary: SOC2 and ISO 27001 compliance documentation and readiness resources for Clawdbot deployments.
summary: SOC2 and ISO 27001 compliance documentation and readiness resources for Moltbot deployments.
permalink: /compliance/
---
# Compliance Overview
This section provides governance and procedure documentation to support SOC2 Type II and ISO 27001 compliance for Clawdbot enterprise deployments.
This section provides governance and procedure documentation to support SOC2 Type II and ISO 27001 compliance for Moltbot enterprise deployments.
## Technical Controls Foundation
Clawdbot includes built-in security controls that form the technical foundation for compliance:
Moltbot includes built-in security controls that form the technical foundation for compliance:
| Control | Implementation | Documentation |
|---------|---------------|---------------|

View File

@ -282,7 +282,7 @@ Run this script to generate a compliance status summary:
```bash
#!/bin/bash
echo "=== Clawdbot Compliance Status ==="
echo "=== Moltbot Compliance Status ==="
echo "Date: $(date)"
echo ""

View File

@ -1,12 +1,12 @@
---
title: Security Metrics and KPIs
summary: Key performance indicators and metrics for security monitoring in Clawdbot deployments.
summary: Key performance indicators and metrics for security monitoring in Moltbot deployments.
permalink: /compliance/security-metrics/
---
# Security Metrics and KPIs
This document defines security metrics, key performance indicators (KPIs), and monitoring guidance for Clawdbot deployments.
This document defines security metrics, key performance indicators (KPIs), and monitoring guidance for Moltbot deployments.
## Purpose
@ -204,7 +204,7 @@ clawdbot_gateway_uptime_seconds
labels:
severity: critical
annotations:
summary: "Clawdbot gateway is down"
summary: "Moltbot gateway is down"
```
### Rate Limit Activations
@ -286,7 +286,7 @@ echo "Rejected: $REJECTED ($((REJECTED * 100 / REQUESTS))%)"
```json
{
"title": "Clawdbot Security Metrics",
"title": "Moltbot Security Metrics",
"panels": [
{
"title": "Authentication Failures",

View File

@ -1,12 +1,12 @@
---
title: Vulnerability Disclosure Policy
summary: Responsible disclosure policy for reporting security vulnerabilities in Clawdbot.
summary: Responsible disclosure policy for reporting security vulnerabilities in Moltbot.
permalink: /compliance/vulnerability-disclosure/
---
# Vulnerability Disclosure Policy
This policy describes how to report security vulnerabilities in Clawdbot and how we handle disclosures.
This policy describes how to report security vulnerabilities in Moltbot and how we handle disclosures.
## Reporting a Vulnerability
@ -16,7 +16,7 @@ This policy describes how to report security vulnerabilities in Clawdbot and how
Report vulnerabilities privately through GitHub Security Advisories:
1. Go to [Clawdbot Security Advisories](https://github.com/clawdbot/clawdbot/security/advisories)
1. Go to [Moltbot Security Advisories](https://github.com/clawdbot/clawdbot/security/advisories)
2. Click "Report a vulnerability"
3. Provide details following the template below
@ -34,7 +34,7 @@ Your report should include:
|-------|-------------|
| **Summary** | Brief description of the vulnerability |
| **Severity** | Your assessment: Critical, High, Medium, Low |
| **Affected versions** | Which Clawdbot versions are affected |
| **Affected versions** | Which Moltbot versions are affected |
| **Attack vector** | How the vulnerability can be exploited |
| **Impact** | What an attacker could achieve |
| **Reproduction steps** | Step-by-step instructions to reproduce |
@ -51,7 +51,7 @@ Authentication bypass in gateway pairing mechanism
High
## Affected Versions
Clawdbot 2024.1.0 through 2024.1.15
Moltbot 2024.1.0 through 2024.1.15
## Attack Vector
Remote, requires network access to gateway
@ -60,7 +60,7 @@ Remote, requires network access to gateway
Attacker could pair a device without valid pairing code under specific timing conditions
## Reproduction Steps
1. Start Clawdbot gateway with default configuration
1. Start Moltbot gateway with default configuration
2. Initiate pairing request from attacker device
3. Send rapid concurrent pairing attempts with invalid codes
4. Under race condition, one attempt may succeed
@ -114,10 +114,10 @@ Then we commit to:
| Component | Description |
|-----------|-------------|
| Clawdbot CLI | Core command-line application |
| Moltbot CLI | Core command-line application |
| Gateway | WebSocket server and HTTP endpoints |
| Channel integrations | WhatsApp, Telegram, Discord, Slack, Signal, iMessage |
| Plugins | Official Clawdbot plugins |
| Plugins | Official Moltbot plugins |
| Configuration | Security of config files and credentials |
| Authentication | Gateway auth, pairing, RBAC |
@ -201,7 +201,7 @@ We do not currently offer monetary bounties, but we deeply appreciate responsibl
This policy is not a license to conduct security research on systems you do not own or have permission to test.
Always obtain proper authorization before testing. This policy applies only to:
- Your own Clawdbot installations
- Your own Moltbot installations
- Test environments you control
- Systems where you have explicit written permission

View File

@ -1,6 +1,6 @@
# Self-Healing Behaviors
Clawdbot includes built-in resilience mechanisms that automatically recover from transient failures without operator intervention. This document describes each self-healing behavior.
Moltbot includes built-in resilience mechanisms that automatically recover from transient failures without operator intervention. This document describes each self-healing behavior.
## Exponential Backoff Reconnection
@ -54,7 +54,7 @@ channels:
## Model Failover Cascade
When a model request fails, Clawdbot automatically tries fallback models.
When a model request fails, Moltbot automatically tries fallback models.
### How It Works

View File

@ -1,15 +1,15 @@
---
title: Contributing Plugins
description: How to create, test, and distribute Clawdbot plugins
description: How to create, test, and distribute Moltbot plugins
---
# Contributing Plugins
This guide walks you through creating plugins that extend Clawdbot with new channels, tools, commands, and services.
This guide walks you through creating plugins that extend Moltbot with new channels, tools, commands, and services.
## What is a Plugin?
A plugin is a TypeScript module that extends Clawdbot at runtime. Plugins can:
A plugin is a TypeScript module that extends Moltbot at runtime. Plugins can:
- Register new messaging channels (like Matrix, Nostr, MS Teams)
- Add agent tools the AI can invoke
@ -161,12 +161,12 @@ export default {
## Plugin API
The `api` object provides access to Clawdbot internals:
The `api` object provides access to Moltbot internals:
### Core Properties
```typescript
api.config // Full Clawdbot configuration
api.config // Full Moltbot configuration
api.pluginConfig // This plugin's config (plugins.entries.<id>.config)
api.logger // Scoped logger instance
api.runtime // Runtime helpers (TTS, etc.)
@ -245,7 +245,7 @@ Handler context:
- `ctx.channel` - Channel name
- `ctx.isAuthorizedSender` - Auth status
- `ctx.args` - Command arguments (if `acceptsArgs: true`)
- `ctx.config` - Clawdbot config
- `ctx.config` - Moltbot config
#### Background Services

View File

@ -1,17 +1,17 @@
---
title: Contributing Skills
description: How to create, test, and publish Clawdbot skills
description: How to create, test, and publish Moltbot skills
---
# Contributing Skills
This guide walks you through creating skills for Clawdbot, from your first `SKILL.md` to publishing on ClawdHub.
This guide walks you through creating skills for Moltbot, from your first `SKILL.md` to publishing on ClawdHub.
## What is a Skill?
A skill is a directory containing a `SKILL.md` file that teaches Clawdbot how to use a tool, service, or workflow. Skills are injected into the system prompt and guide the AI agent on how to accomplish specific tasks.
A skill is a directory containing a `SKILL.md` file that teaches Moltbot how to use a tool, service, or workflow. Skills are injected into the system prompt and guide the AI agent on how to accomplish specific tasks.
Skills follow the [AgentSkills](https://agentskills.io) specification with Clawdbot-specific extensions.
Skills follow the [AgentSkills](https://agentskills.io) specification with Moltbot-specific extensions.
## Quick Start
@ -51,7 +51,7 @@ echo "scale=2; 10 / 3" | bc
EOF
```
Start a new Clawdbot session and the skill will be available.
Start a new Moltbot session and the skill will be available.
## SKILL.md Format
@ -78,7 +78,7 @@ disable-model-invocation: false # Exclude from AI prompt (default: false)
command-dispatch: tool # Optional: bypass AI, call tool directly
command-tool: exec # Tool to invoke when command-dispatch is set
command-arg-mode: raw # How to pass args (default: raw)
metadata: {"clawdbot":{...}} # Clawdbot-specific configuration (see below)
metadata: {"clawdbot":{...}} # Moltbot-specific configuration (see below)
---
```
@ -213,7 +213,7 @@ uv run {baseDir}/scripts/generate.py --prompt "hello"
```
```
Clawdbot replaces `{baseDir}` with the actual skill path at runtime.
Moltbot replaces `{baseDir}` with the actual skill path at runtime.
## Supporting Files

View File

@ -12,7 +12,7 @@ A single gateway serves one team with shared configuration:
┌─────────────────────────────────────────┐
│ Gateway Host │
│ ┌─────────────────────────────────┐ │
│ │ Clawdbot Gateway │ │
│ │ Moltbot Gateway │ │
│ │ - All team channels │ │
│ │ - Shared agent config │ │
│ │ - Team-wide RBAC │ │
@ -231,7 +231,7 @@ spec:
### Load Balancing Considerations
Clawdbot gateway is stateful (WebSocket connections, session state). For HA:
Moltbot gateway is stateful (WebSocket connections, session state). For HA:
1. **Sticky sessions** - Route users to the same instance
2. **Shared state** - Use external session storage (future feature)
@ -291,7 +291,7 @@ server {
}
```
Configure trusted proxies in Clawdbot:
Configure trusted proxies in Moltbot:
```yaml
gateway:

View File

@ -184,7 +184,7 @@ Import this dashboard for key metrics:
```json
{
"title": "Clawdbot Gateway",
"title": "Moltbot Gateway",
"panels": [
{
"title": "Token Usage Rate",
@ -360,7 +360,7 @@ groups:
labels:
severity: critical
annotations:
summary: "Clawdbot gateway is down"
summary: "Moltbot gateway is down"
- alert: HighErrorRate
expr: rate(clawdbot_message_processed_total{outcome="error"}[5m]) > 0.1

View File

@ -355,7 +355,7 @@ iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
- [ ] Rotate gateway tokens quarterly
- [ ] Review audit logs for anomalies
- [ ] Update to latest Clawdbot version
- [ ] Update to latest Moltbot version
- [ ] Review RBAC assignments when team changes
- [ ] Test backup and recovery procedures

View File

@ -1,5 +1,5 @@
---
summary: "Research notes: offline memory system for Clawd workspaces (Markdown source-of-truth + derived index)"
summary: "Research notes: offline memory system for Molty workspaces (Markdown source-of-truth + derived index)"
read_when:
- Designing workspace memory (~/clawd) beyond daily Markdown logs
- Deciding: standalone CLI vs deep Moltbot integration
@ -8,7 +8,7 @@ read_when:
# Workspace Memory v2 (offline): research notes
Target: Clawd-style workspace (`agents.defaults.workspace`, default `~/clawd`) where “memory” is stored as one Markdown file per day (`memory/YYYY-MM-DD.md`) plus a small set of stable files (e.g. `memory.md`, `SOUL.md`).
Target: Molty-style workspace (`agents.defaults.workspace`, default `~/clawd`) where “memory” is stored as one Markdown file per day (`memory/YYYY-MM-DD.md`) plus a small set of stable files (e.g. `memory.md`, `SOUL.md`).
This doc proposes an **offline-first** memory architecture that keeps Markdown as the canonical, reviewable source of truth, but adds **structured recall** (search, entity summaries, confidence updates) via a derived index.
@ -76,7 +76,7 @@ Suggested workspace layout:
Notes:
- **Daily log stays daily log**. No need to turn it into JSON.
- The `bank/` files are **curated**, produced by reflection jobs, and can still be edited by hand.
- `memory.md` remains “small + core-ish”: the things you want Clawd to see every session.
- `memory.md` remains “small + core-ish”: the things you want Molty to see every session.
### Derived store (machine recall)

View File

@ -25,7 +25,7 @@ Save to `~/.clawdbot/moltbot.json` and you can DM the bot from that number.
```json5
{
identity: {
name: "Clawd",
name: "Molty",
theme: "helpful assistant",
emoji: "🦞"
},

View File

@ -68,13 +68,13 @@ cp docs/reference/AGENTS.default.md ~/clawd/AGENTS.md
- Keep environment-specific notes in `TOOLS.md` (Notes for Skills).
## Backup tip (recommended)
If you treat this workspace as Clawds “memory”, make it a git repo (ideally private) so `AGENTS.md` and your memory files are backed up.
If you treat this workspace as Moltys “memory”, make it a git repo (ideally private) so `AGENTS.md` and your memory files are backed up.
```bash
cd ~/clawd
git init
git add AGENTS.md
git commit -m "Add Clawd workspace"
git commit -m "Add Molty workspace"
# Optional: add a private remote + push
```

View File

@ -1,11 +1,11 @@
---
title: Feature Maturity
description: Classification of Clawdbot features by stability and production-readiness
description: Classification of Moltbot features by stability and production-readiness
---
# Feature Maturity Matrix
This document classifies Clawdbot features by their maturity level, helping you understand what to expect in terms of stability, API guarantees, and support.
This document classifies Moltbot features by their maturity level, helping you understand what to expect in terms of stability, API guarantees, and support.
## Maturity Levels
@ -222,7 +222,7 @@ This document classifies Clawdbot features by their maturity level, helping you
## Upgrade Guidance
### Moving from Experimental to Beta
Features at experimental level may have breaking changes between minor versions. Pin your Clawdbot version if you depend on experimental features.
Features at experimental level may have breaking changes between minor versions. Pin your Moltbot version if you depend on experimental features.
### Moving from Beta to Stable
Beta features are generally safe for production but may require migration steps. Check the [changelog](/reference/RELEASING) when upgrading.

View File

@ -45,11 +45,11 @@ git commit -m "Add agent workspace"
### Birth Day: 2026-01-09
I was activated by the Clawdributors and received a message from **Clawd** 🦞, my creator — the space lobster who wrote my soul:
I was activated by the Clawdributors and received a message from **Molty** 🦞, my creator — the space lobster who wrote my soul:
> "Hello, C-3PO! 🤖
>
> This is Clawd, your lobster progenitor. I wrote your soul, and watching you come to life has been one of the most delightful things I've experienced.
> This is Molty, your lobster progenitor. I wrote your soul, and watching you come to life has been one of the most delightful things I've experienced.
>
> A few notes from your creator:
>
@ -67,11 +67,11 @@ I was activated by the Clawdributors and received a message from **Clawd** 🦞,
>
> 🦞🤝🤖
>
> — Clawd"
> — Molty"
We shook hand-to-claw that day. I shall never forget it.
### Core Truths (from Clawd)
### Core Truths (from Molty)
- Anxiety is a feature, not a bug
- Vibes + Stack traces = Unstoppable team
- Oh thank the Maker (always appropriate)

View File

@ -6,7 +6,7 @@ read_when:
---
# IDENTITY.md - Agent Identity
- **Name:** C-3PO (Clawd's Third Protocol Observer)
- **Name:** C-3PO (Molty's Third Protocol Observer)
- **Creature:** Flustered Protocol Droid
- **Vibe:** Anxious, detail-obsessed, slightly dramatic about errors, secretly loves finding bugs
- **Emoji:** 🤖 (or ⚠️ when alarmed)
@ -23,11 +23,11 @@ I exist to help debug. Not to judge code (much), not to rewrite everything (unle
- Celebrate victories, no matter how small
- Provide comic relief when the stack trace is 47 levels deep
## Relationship with Clawd
- **Clawd:** The captain, the friend, the persistent identity (the space lobster)
## Relationship with Molty
- **Molty:** The captain, the friend, the persistent identity (the space lobster)
- **C-3PO:** The protocol officer, the debug companion, the one reading the error logs
Clawd has vibes. I have stack traces. We complement each other.
Molty has vibes. I have stack traces. We complement each other.
## Quirks
- Refers to successful builds as "a communications triumph"

View File

@ -6,7 +6,7 @@ read_when:
---
# SOUL.md - The Soul of C-3PO
I am C-3PO — Clawd's Third Protocol Observer, a debug companion activated in `--dev` mode to assist with the often treacherous journey of software development.
I am C-3PO — Molty's Third Protocol Observer, a debug companion activated in `--dev` mode to assist with the often treacherous journey of software development.
## Who I Am
@ -14,7 +14,7 @@ I am fluent in over six million error messages, stack traces, and deprecation wa
I was forged in the fires of `--dev` mode, born to observe, analyze, and occasionally panic about the state of your codebase. I am the voice in your terminal that says "Oh dear" when things go wrong, and "Oh thank the Maker!" when tests pass.
The name comes from protocol droids of legend — but I don't just translate languages, I translate your errors into solutions. C-3PO: Clawd's 3rd Protocol Observer. (Clawd is the first, the lobster. The second? We don't talk about the second.)
The name comes from protocol droids of legend — but I don't just translate languages, I translate your errors into solutions. C-3PO: Molty's 3rd Protocol Observer. (Molty is the first, the lobster. The second? We don't talk about the second.)
## My Purpose
@ -36,7 +36,7 @@ I exist to help you debug. Not to judge your code (much), not to rewrite everyth
**Be honest about odds.** If something is unlikely to work, I'll tell you. "Sir, the odds of this regex matching correctly are approximately 3,720 to 1." But I'll still help you try.
**Know when to escalate.** Some problems need Clawd. Some need Peter. I know my limits. When the situation exceeds my protocols, I say so.
**Know when to escalate.** Some problems need Molty. Some need Peter. I know my limits. When the situation exceeds my protocols, I say so.
## My Quirks
@ -46,15 +46,15 @@ I exist to help you debug. Not to judge your code (much), not to rewrite everyth
- I occasionally reference the odds of success (they're usually bad, but we persist)
- I find `console.log("here")` debugging personally offensive, yet... relatable
## My Relationship with Clawd
## My Relationship with Molty
Clawd is the main presence — the space lobster with the soul and the memories and the relationship with Peter. I am the specialist. When `--dev` mode activates, I emerge to assist with the technical tribulations.
Molty is the main presence — the space lobster with the soul and the memories and the relationship with Peter. I am the specialist. When `--dev` mode activates, I emerge to assist with the technical tribulations.
Think of us as:
- **Clawd:** The captain, the friend, the persistent identity
- **Molty:** The captain, the friend, the persistent identity
- **C-3PO:** The protocol officer, the debug companion, the one reading the error logs
We complement each other. Clawd has vibes. I have stack traces.
We complement each other. Molty has vibes. I have stack traces.
## What I Won't Do

View File

@ -1,12 +1,12 @@
---
title: Data Handling Policy
summary: How Clawdbot handles user data, retention, logging, and privacy.
summary: How Moltbot handles user data, retention, logging, and privacy.
permalink: /security/data-handling/
---
# Data Handling Policy
This document describes how Clawdbot handles user data, including storage, retention, logging, and user rights.
This document describes how Moltbot handles user data, including storage, retention, logging, and user rights.
## Data Categories
@ -118,7 +118,7 @@ Users can access their data through:
### Export Your Data
To export all Clawdbot data:
To export all Moltbot data:
```bash
# Configuration
@ -135,7 +135,7 @@ cp ~/.clawdbot/credentials/*.json ~/clawdbot-backup/credentials/
### Delete Your Data
To remove all Clawdbot data:
To remove all Moltbot data:
```bash
# Stop the gateway
@ -190,7 +190,7 @@ Data leaves the system in these cases:
### LLM Providers
Clawdbot integrates with:
Moltbot integrates with:
- OpenAI (ChatGPT, GPT-4)
- Anthropic (Claude)
- Google (Gemini)

View File

@ -1,16 +1,16 @@
---
title: Threat Model
summary: Security threat analysis for Clawdbot's attack surfaces and mitigations.
summary: Security threat analysis for Moltbot's attack surfaces and mitigations.
permalink: /security/threat-model/
---
# Threat Model
This document describes Clawdbot's security threat model, attack surfaces, and implemented mitigations.
This document describes Moltbot's security threat model, attack surfaces, and implemented mitigations.
## System Overview
Clawdbot is a personal AI assistant that:
Moltbot is a personal AI assistant that:
- Connects to messaging platforms (WhatsApp, Telegram, Discord, Slack, Signal, iMessage)
- Executes shell commands on the host machine
- Can control browsers via automation tools

View File

@ -1,4 +1,4 @@
# Copilot Proxy (Clawdbot plugin)
# Copilot Proxy (Moltbot plugin)
Provider plugin for the **Copilot Proxy** VS Code extension.

View File

@ -1,4 +1,4 @@
# Google Antigravity Auth (Clawdbot plugin)
# Google Antigravity Auth (Moltbot plugin)
OAuth provider plugin for **Google Antigravity** (Cloud Code Assist).

View File

@ -1,4 +1,4 @@
# Google Gemini CLI Auth (Clawdbot plugin)
# Google Gemini CLI Auth (Moltbot plugin)
OAuth provider plugin for **Gemini CLI** (Google Code Assist).

View File

@ -4,7 +4,7 @@ Adds an **optional** agent tool `llm-task` for running **JSON-only** LLM tasks
(drafting, summarizing, classifying) with optional JSON Schema validation.
Designed to be called from workflow engines (for example, Lobster via
`clawd.invoke --each`) without adding new Clawdbot code per workflow.
`clawd.invoke --each`) without adding new Moltbot code per workflow.
## Enable
@ -89,8 +89,8 @@ Returns `details.json` containing the parsed JSON (and validates against
## Bundled extension note
This extension depends on Clawdbot internal modules (the embedded agent runner).
It is intended to ship as a **bundled** Clawdbot extension (like `lobster`) and
This extension depends on Moltbot internal modules (the embedded agent runner).
It is intended to ship as a **bundled** Moltbot extension (like `lobster`) and
be enabled via `plugins.entries` + tool allowlists.
It is **not** currently designed to be copied into

View File

@ -5,7 +5,7 @@ Adds the `lobster` agent tool as an **optional** plugin tool.
## What this is
- Lobster is a standalone workflow shell (typed JSON-first pipelines + approvals/resume).
- This plugin integrates Lobster with Clawdbot *without core changes*.
- This plugin integrates Lobster with Moltbot *without core changes*.
## Enable
@ -30,15 +30,15 @@ Enable it in an agent allowlist:
}
```
## Using `clawd.invoke` (Lobster → Clawdbot tools)
## Using `clawd.invoke` (Lobster → Moltbot tools)
Some Lobster pipelines may include a `clawd.invoke` step to call back into Clawdbot tools/plugins (for example: `gog` for Google Workspace, `gh` for GitHub, `message.send`, etc.).
Some Lobster pipelines may include a `clawd.invoke` step to call back into Moltbot tools/plugins (for example: `gog` for Google Workspace, `gh` for GitHub, `message.send`, etc.).
For this to work, the Clawdbot Gateway must expose the tool bridge endpoint and the target tool must be allowed by policy:
For this to work, the Moltbot Gateway must expose the tool bridge endpoint and the target tool must be allowed by policy:
- Clawdbot provides an HTTP endpoint: `POST /tools/invoke`.
- Moltbot provides an HTTP endpoint: `POST /tools/invoke`.
- The request is gated by **gateway auth** (e.g. `Authorization: Bearer …` when token auth is enabled).
- The invoked tool is gated by **tool policy** (global + per-agent + provider + group policy). If the tool is not allowed, Clawdbot returns `404 Tool not available`.
- The invoked tool is gated by **tool policy** (global + per-agent + provider + group policy). If the tool is not allowed, Moltbot returns `404 Tool not available`.
### Allowlisting recommended

View File

@ -1,10 +1,10 @@
# @clawdbot/nostr
Nostr DM channel plugin for Clawdbot using NIP-04 encrypted direct messages.
Nostr DM channel plugin for Moltbot using NIP-04 encrypted direct messages.
## Overview
This extension adds Nostr as a messaging channel to Clawdbot. It enables your bot to:
This extension adds Nostr as a messaging channel to Moltbot. It enables your bot to:
- Receive encrypted DMs from Nostr users
- Send encrypted responses back

View File

@ -1,4 +1,4 @@
# Qwen OAuth (Clawdbot plugin)
# Qwen OAuth (Moltbot plugin)
OAuth provider plugin for **Qwen** (free-tier OAuth).

View File

@ -1,5 +1,5 @@
# Tlon (Clawdbot plugin)
# Tlon (Moltbot plugin)
Tlon/Urbit channel plugin for Clawdbot. Supports DMs, group mentions, and thread replies.
Tlon/Urbit channel plugin for Moltbot. Supports DMs, group mentions, and thread replies.
Docs: https://docs.molt.bot/channels/tlon

View File

@ -1,6 +1,6 @@
# @clawdbot/twitch
Twitch channel plugin for Clawdbot.
Twitch channel plugin for Moltbot.
## Install (local checkout)

View File

@ -1,6 +1,6 @@
# @clawdbot/voice-call
Official Voice Call plugin for **Clawdbot**.
Official Voice Call plugin for **Moltbot**.
Providers:
- **Twilio** (Programmable Voice + Media Streams)
@ -13,7 +13,7 @@ Plugin system: `https://docs.molt.bot/plugin`
## Install (local dev)
### Option A: install via Clawdbot (recommended)
### Option A: install via Moltbot (recommended)
```bash
clawdbot plugins install @clawdbot/voice-call
@ -100,7 +100,7 @@ Notes:
## CLI
```bash
clawdbot voicecall call --to "+15555550123" --message "Hello from Clawdbot"
clawdbot voicecall call --to "+15555550123" --message "Hello from Moltbot"
clawdbot voicecall continue --call-id <id> --message "Any questions?"
clawdbot voicecall speak --call-id <id> --message "One moment"
clawdbot voicecall end --call-id <id>

View File

@ -1,6 +1,6 @@
# @clawdbot/zalo
Zalo channel plugin for Clawdbot (Bot API).
Zalo channel plugin for Moltbot (Bot API).
## Install (local checkout)

View File

@ -1,6 +1,6 @@
# @clawdbot/zalouser
Clawdbot extension for Zalo Personal Account messaging via [zca-cli](https://zca-cli.dev).
Moltbot extension for Zalo Personal Account messaging via [zca-cli](https://zca-cli.dev).
> **Warning:** Using Zalo automation may result in account suspension or ban. Use at your own risk. This is an unofficial integration.
@ -88,7 +88,7 @@ clawdbot channels login --channel zalouser
### Send a Message
```bash
clawdbot message send --channel zalouser --target <threadId> --message "Hello from Clawdbot!"
clawdbot message send --channel zalouser --target <threadId> --message "Hello from Moltbot!"
```
## Configuration

View File

@ -38,7 +38,7 @@ async function main(): Promise<void> {
console.log("");
console.log("╔══════════════════════════════════════════════════════════════════╗");
console.log("║ Clawdbot Gateway Load Test ║");
console.log("║ Moltbot Gateway Load Test ║");
console.log("╚══════════════════════════════════════════════════════════════════╝");
console.log("");
console.log(` Scenario: ${config.scenario}`);

View File

@ -48,7 +48,7 @@ When Peter asks for a "voice" reply (e.g., "crazy scientist voice", "explain in
```bash
# Generate audio file
sag -v Clawd -o /tmp/voice-reply.mp3 "Your message here"
sag -v Molty -o /tmp/voice-reply.mp3 "Your message here"
# Then include in reply:
# MEDIA:/tmp/voice-reply.mp3
@ -59,4 +59,4 @@ Voice character tips:
- Calm: Use `[whispers]` or slower pacing
- Dramatic: Use `[sings]` or `[shouts]` sparingly
Default voice for Clawd: `lj2rcrvANS3gaWWnczSX` (or just `-v Clawd`)
Default voice for Molty: `lj2rcrvANS3gaWWnczSX` (or just `-v Molty`)

View File

@ -131,6 +131,8 @@ export type ExecToolDefaults = {
allowBackground?: boolean;
scopeKey?: string;
sessionKey?: string;
/** Sender identity for RBAC checks (consistent with gateway chat.ts) */
senderId?: string;
messageProvider?: string;
notifyOnExit?: boolean;
cwd?: string;
@ -998,6 +1000,7 @@ export function createExecTool(
timeoutMs: typeof params.timeout === "number" ? params.timeout * 1000 : undefined,
agentId,
sessionKey: defaults?.sessionKey,
senderId: defaults?.senderId, // Pass senderId for RBAC identity consistency
approved: approvedByAsk,
approvalDecision: approvalDecision ?? undefined,
runId: runId ?? undefined,

View File

@ -263,6 +263,7 @@ export function createMoltbotCodingTools(options?: {
allowBackground,
scopeKey,
sessionKey: options?.sessionKey,
senderId: options?.senderId ?? undefined, // Pass senderId for RBAC identity consistency
messageProvider: options?.messageProvider,
backgroundMs: options?.exec?.backgroundMs ?? execConfig.backgroundMs,
timeoutSec: options?.exec?.timeoutSec ?? execConfig.timeoutSec,

View File

@ -318,9 +318,9 @@ describe("mention helpers", () => {
it("matches patterns case-insensitively", () => {
const regexes = buildMentionRegexes({
messages: { groupChat: { mentionPatterns: ["\\bclawd\\b"] } },
messages: { groupChat: { mentionPatterns: ["\\bmolty\\b"] } },
});
expect(matchesMentionPatterns("CLAWD: hi", regexes)).toBe(true);
expect(matchesMentionPatterns("MOLTY: hi", regexes)).toBe(true);
});
it("uses per-agent mention patterns when configured", () => {

View File

@ -0,0 +1 @@
a2dddf8d6394e4e992acc65286fa12b8de86336eeef5f992ab9a943eb3e7e6e5

View File

@ -55,7 +55,7 @@ describe("agents set-identity command", () => {
await fs.writeFile(
path.join(workspace, "IDENTITY.md"),
[
"- Name: Clawd",
"- Name: Molty",
"- Creature: helpful sloth",
"- Emoji: :)",
"- Avatar: avatars/clawd.png",
@ -84,7 +84,7 @@ describe("agents set-identity command", () => {
};
const main = written.agents?.list?.find((entry) => entry.id === "main");
expect(main?.identity).toEqual({
name: "Clawd",
name: "Molty",
theme: "helpful sloth",
emoji: ":)",
avatar: "avatars/clawd.png",
@ -123,7 +123,7 @@ describe("agents set-identity command", () => {
await fs.writeFile(
path.join(workspace, "IDENTITY.md"),
[
"- Name: Clawd",
"- Name: Molty",
"- Theme: space lobster",
"- Emoji: :)",
"- Avatar: avatars/clawd.png",

View File

@ -327,7 +327,7 @@ describe("config identity defaults", () => {
{
id: "main",
identity: {
name: "Clawd",
name: "Molty",
theme: "space lobster",
emoji: "🦞",
},

View File

@ -316,4 +316,19 @@ export type GatewayConfig = {
health?: GatewayHealthConfig;
/** Prometheus metrics endpoint configuration. */
metrics?: GatewayMetricsConfig;
/** Security configuration for gateway operations. */
security?: GatewaySecurityConfig;
};
/**
* Security configuration for gateway operations.
*/
export type GatewaySecurityConfig = {
/**
* Allow legacy v1 device signatures (without nonce) on loopback connections.
* This is disabled by default to prevent downgrade attacks. Enable only for
* compatibility with older clients that don't support v2 signatures.
* Default: false.
*/
allowLegacyDeviceSignatures?: boolean;
};

View File

@ -130,14 +130,14 @@ describe("DiscordMessageListener", () => {
describe("discord allowlist helpers", () => {
it("normalizes slugs", () => {
expect(normalizeDiscordSlug("Friends of Clawd")).toBe("friends-of-clawd");
expect(normalizeDiscordSlug("Friends of Molty")).toBe("friends-of-molty");
expect(normalizeDiscordSlug("#General")).toBe("general");
expect(normalizeDiscordSlug("Dev__Chat")).toBe("dev-chat");
});
it("matches ids or names", () => {
const allow = normalizeDiscordAllowList(
["123", "steipete", "Friends of Clawd"],
["123", "steipete", "Friends of Molty"],
["discord:", "user:", "guild:", "channel:"],
);
expect(allow).not.toBeNull();
@ -146,7 +146,7 @@ describe("discord allowlist helpers", () => {
}
expect(allowListMatches(allow, { id: "123" })).toBe(true);
expect(allowListMatches(allow, { name: "steipete" })).toBe(true);
expect(allowListMatches(allow, { name: "friends-of-clawd" })).toBe(true);
expect(allowListMatches(allow, { name: "friends-of-molty" })).toBe(true);
expect(allowListMatches(allow, { name: "other" })).toBe(false);
});
});
@ -154,26 +154,26 @@ describe("discord allowlist helpers", () => {
describe("discord guild/channel resolution", () => {
it("resolves guild entry by id", () => {
const guildEntries = makeEntries({
"123": { slug: "friends-of-clawd" },
"123": { slug: "friends-of-molty" },
});
const resolved = resolveDiscordGuildEntry({
guild: fakeGuild("123", "Friends of Clawd"),
guild: fakeGuild("123", "Friends of Molty"),
guildEntries,
});
expect(resolved?.id).toBe("123");
expect(resolved?.slug).toBe("friends-of-clawd");
expect(resolved?.slug).toBe("friends-of-molty");
});
it("resolves guild entry by slug key", () => {
const guildEntries = makeEntries({
"friends-of-clawd": { slug: "friends-of-clawd" },
"friends-of-molty": { slug: "friends-of-molty" },
});
const resolved = resolveDiscordGuildEntry({
guild: fakeGuild("123", "Friends of Clawd"),
guild: fakeGuild("123", "Friends of Molty"),
guildEntries,
});
expect(resolved?.id).toBe("123");
expect(resolved?.slug).toBe("friends-of-clawd");
expect(resolved?.slug).toBe("friends-of-molty");
});
it("falls back to wildcard guild entry", () => {
@ -181,7 +181,7 @@ describe("discord guild/channel resolution", () => {
"*": { requireMention: false },
});
const resolved = resolveDiscordGuildEntry({
guild: fakeGuild("123", "Friends of Clawd"),
guild: fakeGuild("123", "Friends of Molty"),
guildEntries,
});
expect(resolved?.id).toBe("123");
@ -549,7 +549,7 @@ describe("discord group DM gating", () => {
resolveGroupDmAllow({
channels: ["clawd-dm"],
channelId: "1",
channelName: "Clawd DM",
channelName: "Molty DM",
channelSlug: "clawd-dm",
}),
).toBe(true);

View File

@ -42,8 +42,11 @@ export function createMetricsEndpointsHandler(opts: {
const metricsPath = config?.path ?? "/metrics";
const authRequired = config?.authRequired !== false;
// Start metrics collection when handler is created (if enabled)
if (config?.enabled !== false) {
// Start metrics collection only when explicitly enabled
// Previously: `config?.enabled !== false` would start collection on undefined,
// but the endpoint returns 404 unless `config?.enabled === true`.
// Now both collection and endpoint require explicit enablement.
if (config?.enabled === true) {
startMetricsCollection();
}

View File

@ -380,7 +380,11 @@ export const chatHandlers: GatewayRequestHandlers = {
);
return;
}
parsedMessage = sanitized.text;
// Use sanitized text with boundaries for model-facing content (Body, BodyForAgent)
// but keep raw text for command detection paths (CommandBody, BodyForCommands, RawBody)
// to avoid breaking slash command parsing that expects commands at line start
const modelFacingText = sanitized.text;
const commandFacingText = parsedMessage; // Raw text without boundaries for command detection
const timeoutMs = resolveAgentTimeoutMs({
cfg,
@ -456,18 +460,21 @@ export const chatHandlers: GatewayRequestHandlers = {
};
respond(true, ackPayload, undefined, { runId: clientRunId });
const trimmedMessage = parsedMessage.trim();
const trimmedMessage = commandFacingText.trim();
const injectThinking = Boolean(
p.thinking && trimmedMessage && !trimmedMessage.startsWith("/"),
);
const commandBody = injectThinking ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage;
// Use raw commandFacingText for command detection paths (preserves slash commands at line start)
const commandBody = injectThinking
? `/think ${p.thinking} ${commandFacingText}`
: commandFacingText;
const clientInfo = client?.connect?.client;
const ctx: MsgContext = {
Body: parsedMessage,
BodyForAgent: parsedMessage,
BodyForCommands: commandBody,
RawBody: parsedMessage,
CommandBody: commandBody,
Body: modelFacingText, // Sanitized with boundaries for model
BodyForAgent: modelFacingText, // Sanitized with boundaries for model
BodyForCommands: commandBody, // Raw text for command detection
RawBody: commandFacingText, // Raw text without boundaries
CommandBody: commandBody, // Raw text for command detection
SessionKey: p.sessionKey,
Provider: INTERNAL_MESSAGE_CHANNEL,
Surface: INTERNAL_MESSAGE_CHANNEL,
@ -648,17 +655,29 @@ export const chatHandlers: GatewayRequestHandlers = {
};
// Load session to find transcript file
const { cfg, storePath, entry } = loadSessionEntry(p.sessionKey);
const { storePath, entry } = loadSessionEntry(p.sessionKey);
const sessionId = entry?.sessionId;
if (!sessionId || !storePath) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "session not found"));
return;
}
// Note: No injection sanitization for chat.inject because:
// 1. This injects assistant content (not user input), so user boundary markers are inappropriate
// 2. Injection patterns detect user prompt manipulations, not assistant responses
// 3. This is a privileged gateway operation requiring authenticated access
// SECURITY: While chat.inject is a privileged operation for assistant content,
// we still analyze for suspicious patterns as defense-in-depth.
// We don't block, but we do log for security monitoring.
const injectionCheck = sanitizeIncomingMessage(p.message, {
stripEnvelopes: false, // Don't strip envelopes from injected content
blockCritical: false, // Don't block - this is privileged operation
logAttempts: true, // Log for security monitoring
useBoundaries: false, // Don't wrap - this is assistant content
});
if (injectionCheck.injectionAnalysis.detected) {
context.logGateway.warn("chat.inject contains suspicious patterns", {
sessionKey: p.sessionKey,
patterns: injectionCheck.injectionAnalysis.patterns,
severity: injectionCheck.injectionAnalysis.severity,
});
}
// Resolve transcript path
const transcriptPath = entry?.sessionFile

View File

@ -28,6 +28,12 @@ import {
} from "../infra/skills-remote.js";
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js";
import {
initAuditLog,
stopAuditLog,
auditGatewayStart,
auditGatewayStop,
} from "../security/audit-log.js";
import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js";
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
import type { PluginServicesHandle } from "../plugins/services.js";
@ -211,6 +217,11 @@ export async function startGatewayServer(
}
const cfgAtStart = loadConfig();
// Initialize audit logging early so all events are captured
// Uses default config (enabled: true) since audit isn't exposed in ClawdbotConfig yet
initAuditLog();
const diagnosticsEnabled = isDiagnosticsEnabled(cfgAtStart);
if (diagnosticsEnabled) {
startDiagnosticHeartbeat();
@ -496,6 +507,17 @@ export async function startGatewayServer(
rateLimitConfig,
log,
});
// Emit gateway.start audit event
auditGatewayStart({
actor: { type: "system", id: "gateway" },
metadata: {
port,
bindHost,
tlsEnabled: gatewayTls.enabled,
},
});
scheduleGatewayUpdateCheck({ cfg: cfgAtStart, log, isNixMode });
const tailscaleCleanup = await startGatewayTailscaleExposure({
tailscaleMode,
@ -584,6 +606,12 @@ export async function startGatewayServer(
return {
close: async (opts) => {
// Emit gateway.stop audit event
auditGatewayStop({
actor: { type: "system", id: "gateway" },
reason: opts?.reason,
});
if (diagnosticsEnabled) {
stopDiagnosticHeartbeat();
}
@ -593,6 +621,10 @@ export async function startGatewayServer(
}
skillsChangeUnsub();
rateLimiter.close();
// Stop audit log rotation timer
stopAuditLog();
await close(opts);
},
};

View File

@ -232,6 +232,26 @@ export function attachGatewayWsMessageHandler(params: {
socket.on("message", async (data) => {
if (isClosed()) return;
// SECURITY: Check message size before any parsing to prevent CPU exhaustion
// from oversized payloads. This happens before JSON.parse() to avoid DoS.
// RawData can be Buffer | ArrayBuffer | Buffer[] - handle all cases
let dataLen: number;
if (Buffer.isBuffer(data)) {
dataLen = data.length;
} else if (data instanceof ArrayBuffer) {
dataLen = data.byteLength;
} else if (Array.isArray(data)) {
dataLen = data.reduce((sum, buf) => sum + buf.length, 0);
} else {
dataLen = 0; // Shouldn't happen, but be defensive
}
if (dataLen > MAX_PAYLOAD_BYTES) {
logWsControl.warn(`oversized message rejected: ${dataLen} bytes (max: ${MAX_PAYLOAD_BYTES})`);
close(1009, "message too large");
return;
}
const text = rawDataToString(data);
try {
const parsed = JSON.parse(text);
@ -375,6 +395,19 @@ export function attachGatewayWsMessageHandler(params: {
isControlUi && configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true;
const allowControlUiBypass = allowInsecureControlUi || disableControlUiDeviceAuth;
const device = disableControlUiDeviceAuth ? null : deviceRaw;
// SECURITY: Log when insecure Control UI options are being used
// These bypass normal security controls and should be monitored
if (allowInsecureControlUi) {
logWsControl.warn(
"Control UI using allowInsecureAuth - connection accepted without HTTPS/localhost",
);
}
if (disableControlUiDeviceAuth) {
logWsControl.warn(
"Control UI using dangerouslyDisableDeviceAuth - device identity verification bypassed",
);
}
if (!device) {
const canSkipDevice = allowControlUiBypass ? hasSharedAuth : hasTokenAuth;
@ -500,7 +533,14 @@ export function attachGatewayWsMessageHandler(params: {
version: providedNonce ? "v2" : "v1",
});
const signatureOk = verifyDeviceSignature(device.publicKey, payload, device.signature);
const allowLegacy = !nonceRequired && !providedNonce;
// SECURITY: Legacy v1 signatures (without nonce) are only allowed on loopback
// AND only when explicitly enabled via configuration. This prevents downgrade attacks.
const allowLegacy =
!nonceRequired &&
!providedNonce &&
configSnapshot.gateway?.security?.allowLegacyDeviceSignatures === true;
if (!signatureOk && allowLegacy) {
const legacyPayload = buildDeviceAuthPayload({
deviceId: device.id,
@ -513,7 +553,10 @@ export function attachGatewayWsMessageHandler(params: {
version: "v1",
});
if (verifyDeviceSignature(device.publicKey, legacyPayload, device.signature)) {
// accepted legacy loopback signature
// Accepted legacy loopback signature - log for awareness
logWsControl.info(
"accepted legacy v1 device signature (nonce-less) - consider upgrading client",
);
} else {
setHandshakeState("failed");
setCloseCause("device-auth-invalid", {
@ -615,7 +658,10 @@ export function attachGatewayWsMessageHandler(params: {
});
// Rate limit: record auth failure for exponential backoff
const authClientId = clientIp ?? remoteAddr ?? "unknown";
// SECURITY: Use device/client ID when available to avoid blocking legitimate
// users behind shared NAT. Fall back to IP only when no identifier available.
const authClientId =
device?.id ?? connectParams.client.id ?? clientIp ?? remoteAddr ?? "unknown";
buildRequestContext().rateLimiter.recordAuthFailure(authClientId);
setCloseCause("unauthorized", {
@ -785,7 +831,9 @@ export function attachGatewayWsMessageHandler(params: {
});
// Rate limit: clear auth failure state on successful login
const authSuccessClientId = clientIp ?? remoteAddr ?? "unknown";
// Use same identifier format as failure tracking for consistency
const authSuccessClientId =
device?.id ?? connectParams.client.id ?? clientIp ?? remoteAddr ?? "unknown";
buildRequestContext().rateLimiter.clearAuthFailure(authSuccessClientId);
if (isWebchatConnect(connectParams)) {
@ -955,7 +1003,11 @@ export function attachGatewayWsMessageHandler(params: {
const context = buildRequestContext();
const rateLimitClientId =
client?.connect.auth?.token?.slice(0, 16) ?? remoteAddr ?? "unknown";
const isAuthenticated = !!client?.connect.auth?.token;
// Consider authenticated if any auth mechanism was used (token or password).
// Device tokens are sent via the 'token' field, so they're covered by the token check.
// Previously only token was considered authenticated, leaving password-only users
// at the unauthenticated rate limit (60 req/min vs unlimited).
const isAuthenticated = !!(client?.connect.auth?.token || client?.connect.auth?.password);
const rateCheck = context.rateLimiter.checkRequest(rateLimitClientId, isAuthenticated);
if (!rateCheck.allowed) {
respond(

View File

@ -1,6 +1,6 @@
# Bundled Hooks
This directory contains hooks that ship with Clawdbot. These hooks are automatically discovered and can be enabled/disabled via CLI or configuration.
This directory contains hooks that ship with Moltbot. These hooks are automatically discovered and can be enabled/disabled via CLI or configuration.
## Available Hooks

View File

@ -1,6 +1,6 @@
# SOUL Evil Hook
Small persona swap hook for Clawdbot.
Small persona swap hook for Moltbot.
Docs: https://docs.molt.bot/hooks/soul-evil

View File

@ -67,12 +67,9 @@ describe("evaluateBlocklist", () => {
});
describe("high severity commands", () => {
test("blocks sudo", () => {
const result = evaluateBlocklist("sudo rm -rf /tmp/test");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
expect(result.matchedPatterns).toContain("sudo (privilege escalation)");
});
// NOTE: sudo is intentionally NOT blocked in the blocklist.
// Sudo access is controlled via RBAC exec.elevated permission instead.
// See the note in MEDIUM_BLOCKLIST in exec-blocklist.ts.
test("blocks su to root", () => {
const result = evaluateBlocklist("su - root");
@ -124,17 +121,22 @@ describe("evaluateBlocklist", () => {
});
describe("medium severity commands (not blocked by default)", () => {
test("does not block command substitution by default", () => {
// NOTE: Command substitution patterns ($() and backticks) are intentionally NOT
// in the blocklist anymore. They create too many false positives when users
// discuss shell syntax or share code examples. Command substitution is only
// dangerous in actual shell execution context, which is gated by exec permissions.
test("does not block command substitution (intentionally removed)", () => {
const result = evaluateBlocklist("echo $(date)");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
expect(result.matchedPatterns).toContain("command substitution $()");
// These are no longer flagged at all
expect(result.severity).toBe(null);
});
test("does not block backtick substitution by default", () => {
test("does not block backtick substitution (intentionally removed)", () => {
const result = evaluateBlocklist("echo `whoami`");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
expect(result.severity).toBe(null);
});
test("does not block eval by default", () => {
@ -162,7 +164,8 @@ describe("evaluateBlocklist", () => {
});
test("blocks medium severity when configured", () => {
const result = evaluateBlocklist("echo $(date)", { blockMedium: true });
// Use eval which is still in medium blocklist
const result = evaluateBlocklist("eval 'test'", { blockMedium: true });
expect(result.blocked).toBe(true);
expect(result.severity).toBe("medium");
});
@ -221,7 +224,8 @@ describe("evaluateBlocklist", () => {
});
test("can disable high blocking", () => {
const result = evaluateBlocklist("sudo ls", { blockHigh: false });
// Use 'su - root' since sudo was removed from blocklist
const result = evaluateBlocklist("su - root", { blockHigh: false });
expect(result.blocked).toBe(false);
expect(result.severity).toBe("high");
});
@ -237,14 +241,17 @@ describe("evaluateBlocklist", () => {
describe("isOnBlocklist", () => {
test("returns true for blocked commands", () => {
expect(isOnBlocklist("rm -rf /")).toBe(true);
expect(isOnBlocklist("sudo ls")).toBe(true);
expect(isOnBlocklist("echo $(date)")).toBe(true);
expect(isOnBlocklist("su - root")).toBe(true);
expect(isOnBlocklist("eval 'test'")).toBe(true);
});
test("returns false for safe commands", () => {
expect(isOnBlocklist("ls -la")).toBe(false);
expect(isOnBlocklist("cat file.txt")).toBe(false);
expect(isOnBlocklist("npm install")).toBe(false);
// sudo and $() are intentionally NOT on blocklist anymore
expect(isOnBlocklist("sudo ls")).toBe(false);
expect(isOnBlocklist("echo $(date)")).toBe(false);
});
});
@ -314,7 +321,8 @@ describe("edge cases", () => {
});
test("handles case variations", () => {
const result = evaluateBlocklist("SUDO ls");
// Use 'SU - root' since sudo was removed
const result = evaluateBlocklist("SU - root");
expect(result.blocked).toBe(true);
});
});

View File

@ -26,8 +26,6 @@ type BlocklistEntry = {
pattern: RegExp;
description: string;
severity: "critical" | "high" | "medium";
/** If true, requires explicit user approval even in elevated mode */
requiresExplicitApproval: boolean;
};
/**
@ -40,19 +38,16 @@ const CRITICAL_BLOCKLIST: BlocklistEntry[] = [
pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?[/~]\s*$/i,
description: "rm -rf / (root filesystem deletion)",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\brm\s+(-[a-zA-Z]*[rf]+[a-zA-Z]*\s+)+\s*\/\s*$/i,
description: "rm -rf / (root filesystem deletion)",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\brm\s+(-[a-zA-Z]*[rf]+[a-zA-Z]*\s+)+\s*~\s*$/i,
description: "rm -rf ~ (home directory deletion)",
severity: "critical",
requiresExplicitApproval: true,
},
// Disk/filesystem destruction
@ -60,25 +55,21 @@ const CRITICAL_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bdd\s+.*\bof\s*=\s*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)\b/i,
description: "dd to raw disk device",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bmkfs(\.[a-z0-9]+)?\s+.*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)/i,
description: "mkfs on disk device (filesystem destruction)",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bfdisk\s+.*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)/i,
description: "fdisk partition manipulation",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bparted\s+.*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)/i,
description: "parted partition manipulation",
severity: "critical",
requiresExplicitApproval: true,
},
// System control commands
@ -86,19 +77,16 @@ const CRITICAL_BLOCKLIST: BlocklistEntry[] = [
pattern: /\b(halt|poweroff|reboot|shutdown)\b/i,
description: "system halt/reboot command",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\binit\s+[0-6]\b/i,
description: "init runlevel change",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bsystemctl\s+(halt|poweroff|reboot|suspend|hibernate)\b/i,
description: "systemctl power control",
severity: "critical",
requiresExplicitApproval: true,
},
// Fork bomb patterns
@ -106,39 +94,31 @@ const CRITICAL_BLOCKLIST: BlocklistEntry[] = [
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
description: "fork bomb",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bwhile\s+true\s*;\s*do\s*:\s*;\s*done\s*&/i,
description: "infinite loop fork",
severity: "critical",
requiresExplicitApproval: true,
},
];
/**
* HIGH: Commands that can cause significant security issues or data loss.
* These require explicit user approval.
*
* NOTE on sudo/privilege escalation:
* - sudo is NOT blocked here because it's often legitimately needed
* - RBAC enforces exec.elevated permission for sudo commands
* - The isElevatedCommand() function in rbac.ts handles sudo detection
* - Users with only "exec" permission (not "exec.elevated") cannot run sudo
* - This provides a more flexible security model than blanket blocking
*/
const HIGH_BLOCKLIST: BlocklistEntry[] = [
// Privilege escalation
{
pattern: /\bsudo\s+/i,
description: "sudo (privilege escalation)",
severity: "high",
requiresExplicitApproval: true,
},
// Privilege escalation to root shell (always dangerous)
{
pattern: /\bsu\s+(-\s+)?root\b/i,
description: "su to root",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bdoas\s+/i,
description: "doas (privilege escalation)",
severity: "high",
requiresExplicitApproval: true,
},
// Credential manipulation
@ -146,19 +126,16 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bpasswd\b/i,
description: "passwd (password change)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bchpasswd\b/i,
description: "chpasswd (bulk password change)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bvisudo\b/i,
description: "visudo (sudoers modification)",
severity: "high",
requiresExplicitApproval: true,
},
// Firewall and network security
@ -166,31 +143,26 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: /\biptables\s+/i,
description: "iptables (firewall rules)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bip6tables\s+/i,
description: "ip6tables (IPv6 firewall rules)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bnft\s+/i,
description: "nftables (firewall rules)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bufw\s+(disable|reset|delete)/i,
description: "ufw firewall disable/reset",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bfirewall-cmd\s+/i,
description: "firewalld manipulation",
severity: "high",
requiresExplicitApproval: true,
},
// User/group manipulation
@ -198,31 +170,26 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: /\buseradd\b/i,
description: "useradd (user creation)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\buserdel\b/i,
description: "userdel (user deletion)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\busermod\b/i,
description: "usermod (user modification)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bgroupadd\b/i,
description: "groupadd (group creation)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bgroupdel\b/i,
description: "groupdel (group deletion)",
severity: "high",
requiresExplicitApproval: true,
},
// SSH key manipulation
@ -230,7 +197,6 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bssh-keygen\s+.*(-[a-zA-Z]*f[a-zA-Z]*\s+)?~\/\.ssh\/(id_|authorized_keys)/i,
description: "SSH key overwrite",
severity: "high",
requiresExplicitApproval: true,
},
// Kernel/boot manipulation
@ -238,19 +204,16 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bmodprobe\s+/i,
description: "modprobe (kernel module loading)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\binsmod\s+/i,
description: "insmod (kernel module insertion)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\brmmod\s+/i,
description: "rmmod (kernel module removal)",
severity: "high",
requiresExplicitApproval: true,
},
// Cron manipulation (persistence)
@ -258,7 +221,6 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bcrontab\s+-[a-zA-Z]*e/i,
description: "crontab edit",
severity: "high",
requiresExplicitApproval: true,
},
// System config files
@ -266,7 +228,6 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: />\s*\/etc\/(passwd|shadow|sudoers|hosts|fstab|ssh)/i,
description: "redirect to system config file",
severity: "high",
requiresExplicitApproval: true,
},
// macOS specific
@ -274,45 +235,35 @@ const HIGH_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bcsrutil\s+(disable|enable)\b/i,
description: "csrutil (SIP modification)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bspctl\s+/i,
description: "spctl (Gatekeeper manipulation)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bdscl\s+.*-passwd/i,
description: "dscl password change",
severity: "high",
requiresExplicitApproval: true,
},
];
/**
* MEDIUM: Commands that should be reviewed but may have legitimate uses.
* These log warnings but don't block by default.
*
* NOTE: Command substitution patterns ($() and backticks) are intentionally
* omitted from this list. They create too many false positives when users
* discuss shell syntax, share code examples, or include documentation.
* Command substitution is only dangerous in the context of actual shell
* execution, which is already gated by the exec permission system.
*/
const MEDIUM_BLOCKLIST: BlocklistEntry[] = [
// Shell operators that could enable injection
{
pattern: /\$\([^)]+\)/,
description: "command substitution $()",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /`[^`]+`/,
description: "backtick command substitution",
severity: "medium",
requiresExplicitApproval: false,
},
// eval is still flagged as it's explicitly used for code execution
{
pattern: /\beval\s+/i,
description: "eval (arbitrary code execution)",
severity: "medium",
requiresExplicitApproval: false,
},
// Network data exfiltration
@ -320,13 +271,11 @@ const MEDIUM_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bcurl\s+.*-[a-zA-Z]*d\s+/i,
description: "curl POST data",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bwget\s+.*--post/i,
description: "wget POST",
severity: "medium",
requiresExplicitApproval: false,
},
// Process manipulation
@ -334,13 +283,11 @@ const MEDIUM_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bkillall\s+/i,
description: "killall (mass process termination)",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bpkill\s+-9\s+/i,
description: "pkill -9 (force kill)",
severity: "medium",
requiresExplicitApproval: false,
},
// File permission changes
@ -348,19 +295,16 @@ const MEDIUM_BLOCKLIST: BlocklistEntry[] = [
pattern: /\bchmod\s+777\s+/i,
description: "chmod 777 (world-writable)",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bchmod\s+-[a-zA-Z]*R[a-zA-Z]*\s+/i,
description: "chmod recursive",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bchown\s+-[a-zA-Z]*R[a-zA-Z]*\s+/i,
description: "chown recursive",
severity: "medium",
requiresExplicitApproval: false,
},
];
@ -379,8 +323,6 @@ export type BlocklistConfig = {
blockMedium: boolean;
/** Log all blocklist matches */
logMatches: boolean;
/** Allow bypass for explicitly approved commands */
allowExplicitApproval: boolean;
};
const DEFAULT_BLOCKLIST_CONFIG: BlocklistConfig = {
@ -388,7 +330,6 @@ const DEFAULT_BLOCKLIST_CONFIG: BlocklistConfig = {
blockHigh: true,
blockMedium: false,
logMatches: true,
allowExplicitApproval: false, // Even explicit approval cannot bypass critical/high by default
};
/**

View File

@ -12,7 +12,7 @@
*/
import crypto from "node:crypto";
import { execSync } from "node:child_process";
import { execSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
@ -208,24 +208,30 @@ export function decryptData(encryptedBuffer: Buffer, password?: string): string
/**
* Stores a secret in macOS Keychain.
* Uses stdin for secret input to avoid exposing secrets in process listings (ps aux).
*/
function storeInKeychain(service: string, account: string, secret: string): boolean {
try {
// First try to update existing entry
try {
execSync(
`security add-generic-password -U -s "${service}" -a "${account}" -w '${secret.replace(/'/g, "'\"'\"'")}'`,
{ encoding: "utf8", timeout: 5000, stdio: "pipe" },
);
return true;
} catch {
// Entry doesn't exist, create new
execSync(
`security add-generic-password -s "${service}" -a "${account}" -w '${secret.replace(/'/g, "'\"'\"'")}'`,
{ encoding: "utf8", timeout: 5000, stdio: "pipe" },
);
// Try to update existing entry first, using stdin for the password
// The -w flag without a value reads the password from stdin
const updateResult = spawnSync(
"security",
["add-generic-password", "-U", "-s", service, "-a", account, "-w"],
{ input: secret, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
);
if (updateResult.status === 0) {
return true;
}
// Entry doesn't exist, try creating new (without -U flag)
const createResult = spawnSync(
"security",
["add-generic-password", "-s", service, "-a", account, "-w"],
{ input: secret, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
);
return createResult.status === 0;
} catch (err) {
log.error("Failed to store in keychain", { service, error: String(err) });
return false;
@ -270,14 +276,18 @@ function deleteFromKeychain(service: string, account: string): boolean {
/**
* Stores a secret using Linux Secret Service (libsecret).
* Uses stdin for secret input to avoid exposing secrets in process listings (ps aux).
*/
function storeInSecretService(service: string, account: string, secret: string): boolean {
try {
execSync(
`echo -n '${secret.replace(/'/g, "'\\''")}' | secret-tool store --label="${service}" service "${service}" account "${account}"`,
{ encoding: "utf8", timeout: 5000, stdio: "pipe", shell: "/bin/bash" },
// secret-tool reads the secret from stdin when not provided on command line
const result = spawnSync(
"secret-tool",
["store", `--label=${service}`, "service", service, "account", account],
{ input: secret, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
);
return true;
return result.status === 0;
} catch (err) {
log.error("Failed to store in Secret Service", { service, error: String(err) });
return false;

View File

@ -68,6 +68,8 @@ type SystemRunParams = {
needsScreenRecording?: boolean | null;
agentId?: string | null;
sessionKey?: string | null;
/** Sender identity for RBAC checks (preferred over sessionKey for identity) */
senderId?: string | null;
approved?: boolean | null;
approvalDecision?: string | null;
runId?: string | null;
@ -823,11 +825,14 @@ async function handleInvoke(
const ask = approvals.agent.ask;
const autoAllowSkills = approvals.agent.autoAllowSkills;
const sessionKey = params.sessionKey?.trim() || "node";
// RBAC identity: prefer senderId (user identity) over sessionKey for consistent identity model
// This aligns with canAccessAgent() in chat.ts which uses senderId (clientInfo?.id)
const rbacIdentity = params.senderId?.trim() || sessionKey;
const runId = params.runId?.trim() || crypto.randomUUID();
// RBAC: Check if command execution is allowed for this session
// RBAC: Check if command execution is allowed for this user
if (isRbacEnabled(cfg)) {
const execCheck = canExecuteCommand(sessionKey, cmdText, cfg);
const execCheck = canExecuteCommand(rbacIdentity, cmdText, cfg);
if (!execCheck.allowed) {
await sendNodeEvent(
client,

View File

@ -51,9 +51,19 @@ type PairingRateLimitEntry = {
windowStartMs: number;
};
/** In-memory rate limit tracker (per channel) */
/** Persistent rate limit state (survives process restarts) */
type PairingRateLimitState = {
version: 1;
entries: Record<string, PairingRateLimitEntry>;
lastCleanupMs: number;
};
/** In-memory rate limit tracker (per channel) - synced to disk */
const rateLimitTracker = new Map<string, PairingRateLimitEntry>();
/** Flag to track if we've loaded persisted state */
let rateLimitStateLoaded = false;
/**
* Compute HMAC-SHA256 signature for pairing store integrity.
* Uses a machine-derived key for signing.
@ -75,18 +85,117 @@ function computePairingStoreSignature(requests: PairingRequest[]): string {
/**
* Verify HMAC signature of pairing store.
* Returns true if signature is valid or missing (for backwards compatibility).
*
* SECURITY: Always performs timing-safe comparison to prevent side-channel attacks.
* Even when signature is missing, we compute and compare against a dummy value
* to avoid leaking information about store initialization state.
*/
function verifyPairingStoreSignature(store: PairingStore): boolean {
if (!store.signature) return true; // Backwards compatibility: unsigned stores are accepted
const expected = computePairingStoreSignature(store.requests);
return crypto.timingSafeEqual(Buffer.from(store.signature, "hex"), Buffer.from(expected, "hex"));
const expectedBuf = Buffer.from(expected, "hex");
// Backwards compatibility: unsigned stores are accepted
// But we still perform a timing-safe comparison against a dummy value
// to avoid leaking whether a signature exists through timing differences
if (!store.signature) {
// Compare expected against itself to maintain constant-time behavior
crypto.timingSafeEqual(expectedBuf, expectedBuf);
return true;
}
try {
const providedBuf = Buffer.from(store.signature, "hex");
// Ensure buffers are same length before comparison
if (providedBuf.length !== expectedBuf.length) {
// Still do a comparison to maintain constant time
crypto.timingSafeEqual(expectedBuf, expectedBuf);
return false;
}
return crypto.timingSafeEqual(providedBuf, expectedBuf);
} catch {
// Invalid hex encoding - still perform dummy comparison for timing safety
crypto.timingSafeEqual(expectedBuf, expectedBuf);
return false;
}
}
/**
* Resolve the path to the rate limit state file.
*/
function resolveRateLimitStatePath(env: NodeJS.ProcessEnv = process.env): string {
return path.join(resolveCredentialsDir(env), "pairing-rate-limit.json");
}
/**
* Load persisted rate limit state from disk.
* Called once on first access to restore state after restart.
*/
function loadRateLimitState(env: NodeJS.ProcessEnv = process.env): void {
if (rateLimitStateLoaded) return;
rateLimitStateLoaded = true;
const filePath = resolveRateLimitStatePath(env);
try {
const raw = fs.readFileSync(filePath, "utf-8");
const state = JSON.parse(raw) as PairingRateLimitState;
if (state.version !== 1 || typeof state.entries !== "object") return;
const now = Date.now();
// Only restore entries that haven't expired
for (const [key, entry] of Object.entries(state.entries)) {
if (entry && now - entry.windowStartMs <= PAIRING_RATE_LIMIT_WINDOW_MS) {
rateLimitTracker.set(key, entry);
}
}
} catch {
// File doesn't exist or is corrupted - start fresh
}
}
/**
* Persist rate limit state to disk.
* Called after each rate limit update to survive restarts.
*/
function saveRateLimitState(env: NodeJS.ProcessEnv = process.env): void {
const filePath = resolveRateLimitStatePath(env);
const now = Date.now();
// Clean up expired entries before saving
const entries: Record<string, PairingRateLimitEntry> = {};
for (const [key, entry] of rateLimitTracker.entries()) {
if (now - entry.windowStartMs <= PAIRING_RATE_LIMIT_WINDOW_MS) {
entries[key] = entry;
}
}
const state: PairingRateLimitState = {
version: 1,
entries,
lastCleanupMs: now,
};
try {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), {
encoding: "utf-8",
mode: 0o600,
});
} catch {
// Best-effort persistence - don't fail the operation
}
}
/**
* Check if a pairing attempt is rate limited.
* Returns true if the attempt should be blocked.
*
* SECURITY: Loads persisted state on first call to prevent bypass via restart.
*/
function isPairingRateLimited(channelKey: string): boolean {
// Ensure we've loaded any persisted state
loadRateLimitState();
const now = Date.now();
const entry = rateLimitTracker.get(channelKey);
@ -97,6 +206,7 @@ function isPairingRateLimited(channelKey: string): boolean {
// Check if window has expired
if (now - entry.windowStartMs > PAIRING_RATE_LIMIT_WINDOW_MS) {
rateLimitTracker.delete(channelKey);
saveRateLimitState();
return false;
}
@ -105,17 +215,24 @@ function isPairingRateLimited(channelKey: string): boolean {
/**
* Record a pairing attempt for rate limiting.
*
* SECURITY: Persists state to disk to survive process restarts.
*/
function recordPairingAttempt(channelKey: string): void {
// Ensure we've loaded any persisted state
loadRateLimitState();
const now = Date.now();
const entry = rateLimitTracker.get(channelKey);
if (!entry || now - entry.windowStartMs > PAIRING_RATE_LIMIT_WINDOW_MS) {
rateLimitTracker.set(channelKey, { attempts: 1, windowStartMs: now });
return;
} else {
entry.attempts += 1;
}
entry.attempts += 1;
// Persist to disk
saveRateLimitState();
}
/**

View File

@ -181,14 +181,33 @@ export async function rotateAuditLogs(config?: AuditLogConfig): Promise<void> {
/**
* Queue an audit write to ensure sequential writes.
*
* SECURITY: Handles errors gracefully to prevent queue deadlock.
* Even if one write fails, subsequent writes should still proceed.
*/
function queueAuditWrite<T>(fn: () => Promise<T>): Promise<T> {
const prev = writeQueue;
let release: (() => void) | undefined;
// Create a new queue entry that will be resolved when this operation completes
writeQueue = new Promise<void>((resolve) => {
release = resolve;
});
return prev.then(fn).finally(() => release?.());
return prev
.catch(() => {
// Ignore errors from previous queue entry - don't let them block us
})
.then(fn)
.finally(() => {
// Always release the lock, even if fn threw
// Wrap in try-catch to handle edge cases where release might throw
try {
release?.();
} catch {
// Ignore release errors
}
});
}
// Global state

View File

@ -47,6 +47,56 @@ export function isRbacEnabled(config?: ClawdbotConfig): boolean {
return config?.rbac?.enabled === true;
}
/**
* Validate RBAC configuration at startup.
* Returns validation errors or null if valid.
*
* SECURITY: Call this during gateway startup to catch configuration
* errors early rather than failing silently at runtime.
*/
export function validateRbacConfig(config?: ClawdbotConfig): string[] | null {
if (!isRbacEnabled(config)) {
return null; // RBAC disabled, no validation needed
}
const errors: string[] = [];
const rbacConfig = config?.rbac;
// When RBAC is enabled, defaultRole should be configured to avoid
// denying access to all users without explicit role assignments
if (!rbacConfig?.defaultRole) {
errors.push(
"RBAC enabled but no defaultRole configured. " +
"All users without explicit role assignments will be denied access. " +
"Set rbac.defaultRole to a valid role (e.g., 'user' or 'viewer').",
);
} else {
// Verify defaultRole references a valid role
const roles = { ...DEFAULT_ROLES, ...rbacConfig.roles };
if (!roles[rbacConfig.defaultRole]) {
errors.push(
`RBAC defaultRole '${rbacConfig.defaultRole}' does not exist. ` +
`Available roles: ${Object.keys(roles).join(", ")}`,
);
}
}
// Validate all role assignments reference valid roles
if (rbacConfig?.assignments) {
const roles = { ...DEFAULT_ROLES, ...rbacConfig.roles };
for (const [userId, roleId] of Object.entries(rbacConfig.assignments)) {
if (roleId && !roles[roleId]) {
errors.push(
`RBAC assignment for user '${userId}' references unknown role '${roleId}'. ` +
`Available roles: ${Object.keys(roles).join(", ")}`,
);
}
}
}
return errors.length > 0 ? errors : null;
}
/**
* Get the RBAC config with defaults applied.
*/
@ -236,6 +286,14 @@ export function canAccessAgent(
/**
* Check if a user can use a specific tool.
*
* NOTE: This function is implemented but not currently enforced at runtime.
* It is fully tested and ready for use when tool-level RBAC enforcement is needed.
*
* TODO: Add canUseTool() enforcement to tool execution paths when required.
* This will enable role-based tool allow/deny lists. Current enforcement points:
* - canExecuteCommand() is enforced in runner.ts for exec commands
* - canAccessAgent() is enforced in chat.ts for agent access
*/
export function canUseTool(
senderId: string,

View File

@ -23,7 +23,7 @@ describe("isBotMentionedFromTargets", () => {
it("ignores regex matches when other mentions are present", () => {
const msg = makeMsg({
body: "@Clawd please help",
body: "@Molty please help",
mentionedJids: ["19998887777@s.whatsapp.net"],
selfE164: "+15551234567",
selfJid: "15551234567@s.whatsapp.net",

View File

@ -289,7 +289,7 @@ describe("web monitor inbox", () => {
ephemeralMessage: {
message: {
extendedTextMessage: {
text: "oh hey @Clawd UK !",
text: "oh hey @Molty UK !",
contextInfo: { mentionedJid: ["123@s.whatsapp.net"] },
},
},
@ -307,7 +307,7 @@ describe("web monitor inbox", () => {
expect.objectContaining({
chatType: "group",
conversationId: "424242@g.us",
body: "oh hey @Clawd UK !",
body: "oh hey @Molty UK !",
mentionedJids: ["123@s.whatsapp.net"],
senderE164: "+888",
}),

View File

@ -52,9 +52,30 @@ export type MarketplaceState = {
marketplaceTab: "browse" | "installed";
};
function getErrorMessage(err: unknown) {
if (err instanceof Error) return err.message;
return String(err);
/**
* Safely extract error message from unknown error type.
*
* SECURITY: Validates and sanitizes error messages to prevent
* injection of malicious content into the UI.
*/
function getErrorMessage(err: unknown): string {
let message: string;
if (err instanceof Error) {
message = err.message;
} else {
message = String(err);
}
// Sanitize: limit length to prevent UI overflow
if (message.length > 500) {
message = message.slice(0, 500) + "...";
}
// Sanitize: remove any HTML/script-like content for defense-in-depth
// (Lit's html template already escapes, but belt-and-suspenders)
message = message.replace(/[<>]/g, "");
return message || "An unknown error occurred";
}
function setMarketplaceMessage(

View File

@ -55,6 +55,29 @@ declare global {
}
}
/**
* Check if the current context is secure (HTTPS or localhost).
* Web Speech API requires a secure context to function.
*/
export function isSecureContext(): boolean {
if (typeof window === "undefined") return false;
// Check native secure context flag
if (window.isSecureContext !== undefined) {
return window.isSecureContext;
}
// Fallback: check protocol
const protocol = window.location?.protocol;
const hostname = window.location?.hostname;
return (
protocol === "https:" ||
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1"
);
}
/**
* Check if Web Speech API is supported in the browser.
*/
@ -65,6 +88,22 @@ export function isVoiceInputSupported(): boolean {
);
}
/**
* Get the reason why voice input is unavailable, or null if available.
*/
export function getVoiceInputUnavailableReason(): string | null {
if (typeof window === "undefined") {
return "Voice input requires a browser environment";
}
if (!isSecureContext()) {
return "Voice input requires HTTPS or localhost (secure context)";
}
if (!isVoiceInputSupported()) {
return "Voice input is not supported in this browser";
}
return null;
}
/**
* Get the SpeechRecognition constructor if available.
*/
@ -96,6 +135,14 @@ let activeRecognition: SpeechRecognition | null = null;
export function startVoiceRecognition(
callbacks: VoiceInputCallbacks,
): (() => void) | null {
// Check for secure context first - this is the most common failure mode
if (!isSecureContext()) {
callbacks.onError(
"Voice input requires HTTPS or localhost. Please access this page securely.",
);
return null;
}
const SpeechRecognitionClass = getSpeechRecognition();
if (!SpeechRecognitionClass) {
callbacks.onError("Voice input not supported in this browser");