security: comprehensive pre-PR security audit fixes (rebased)
This commit is contained in:
parent
01b5a757d9
commit
5aae2aa197
@ -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/
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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):
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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**.
|
||||
|
||||
|
||||
@ -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\""
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Clawdbot (iOS)
|
||||
# Moltbot (iOS)
|
||||
|
||||
Internal-only SwiftUI app scaffold.
|
||||
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -110,7 +110,7 @@ struct AgentWorkspaceTests {
|
||||
try """
|
||||
# IDENTITY.md - Agent Identity
|
||||
|
||||
- Name: Clawd
|
||||
- Name: Molty
|
||||
- Creature: Space Lobster
|
||||
- Vibe: Helpful
|
||||
- Emoji: lobster
|
||||
|
||||
42
apps/shared/MoltbotKit/Package.resolved
Normal file
42
apps/shared/MoltbotKit/Package.resolved
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 |
|
||||
|---------|---------------|---------------|
|
||||
|
||||
@ -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 ""
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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: "🦞"
|
||||
},
|
||||
|
||||
@ -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 Clawd’s “memory”, make it a git repo (ideally private) so `AGENTS.md` and your memory files are backed up.
|
||||
If you treat this workspace as Molty’s “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
|
||||
```
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Copilot Proxy (Clawdbot plugin)
|
||||
# Copilot Proxy (Moltbot plugin)
|
||||
|
||||
Provider plugin for the **Copilot Proxy** VS Code extension.
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Google Antigravity Auth (Clawdbot plugin)
|
||||
# Google Antigravity Auth (Moltbot plugin)
|
||||
|
||||
OAuth provider plugin for **Google Antigravity** (Cloud Code Assist).
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Qwen OAuth (Clawdbot plugin)
|
||||
# Qwen OAuth (Moltbot plugin)
|
||||
|
||||
OAuth provider plugin for **Qwen** (free-tier OAuth).
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# @clawdbot/twitch
|
||||
|
||||
Twitch channel plugin for Clawdbot.
|
||||
Twitch channel plugin for Moltbot.
|
||||
|
||||
## Install (local checkout)
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# @clawdbot/zalo
|
||||
|
||||
Zalo channel plugin for Clawdbot (Bot API).
|
||||
Zalo channel plugin for Moltbot (Bot API).
|
||||
|
||||
## Install (local checkout)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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`)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
1
src/canvas-host/a2ui/.bundle.hash
Normal file
1
src/canvas-host/a2ui/.bundle.hash
Normal file
@ -0,0 +1 @@
|
||||
a2dddf8d6394e4e992acc65286fa12b8de86336eeef5f992ab9a943eb3e7e6e5
|
||||
@ -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",
|
||||
|
||||
@ -327,7 +327,7 @@ describe("config identity defaults", () => {
|
||||
{
|
||||
id: "main",
|
||||
identity: {
|
||||
name: "Clawd",
|
||||
name: "Molty",
|
||||
theme: "space lobster",
|
||||
emoji: "🦞",
|
||||
},
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
}),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user