Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ DevBridge turns your Telegram into a powerful developer control panel. Chat with

- 🤖 **AI Chat via Telegram** -- Send messages to Claude CLI or Gemini CLI and get responses right in Telegram
- 📂 **Multi-Project Support** -- Switch between multiple codebases on the fly with `/project`
- 🔒 **Security First** -- Chat ID whitelist, command sandbox with whitelist-only execution, read-only AI tools
- 🔒 **Security First** -- Chat ID whitelist, command sandbox with whitelist-only execution, configurable AI permission levels (`readonly`, `read-write`, `full`)
- 🔌 **Plugin System** -- Extend functionality with built-in or custom plugins (Git, GitHub, and more)
- 🏃 **Run Commands** -- Execute pre-approved shell commands (`/run test`, `/run build`) safely
- 💬 **Session Management** -- Persistent conversation sessions per project with automatic TTL cleanup
Expand Down Expand Up @@ -72,12 +72,14 @@ DevBridge is configured via `devbridge.config.json` in your working directory. R
"path": "/absolute/path/to/project", // Absolute path to project root
"adapter": "claude", // "claude" or "gemini"
"model": "sonnet", // Optional: model override
"description": "My main app" // Optional: shown in /projects
"description": "My main app", // Optional: shown in /projects
"permission_level": "read-write" // Optional: "readonly" (default), "read-write", or "full"
},
"api-backend": {
"path": "/absolute/path/to/api",
"adapter": "gemini",
"description": "Backend API"
// permission_level defaults to "readonly"
}
},

Expand All @@ -100,7 +102,9 @@ DevBridge is configured via `devbridge.config.json` in your working directory. R
"defaults": {
"adapter": "claude", // Default adapter for new projects
"model": "sonnet", // Default model (adapter-specific)
"timeout": 120, // AI response timeout in seconds
"timeout": 120, // AI response timeout in seconds (non-streaming)
"stream_timeout": 3600, // Hard max seconds for streaming responses (1 hour)
"inactivity_timeout": 300, // Kill streaming process after N seconds of no output
"max_message_length": 4096, // Telegram message chunk size
"session_ttl_hours": 24, // Auto-cleanup inactive sessions
"command_timeout": 60 // /run command timeout in seconds
Expand Down Expand Up @@ -181,7 +185,7 @@ See [docs/configuration.md](docs/configuration.md) for detailed documentation of

### Chat

Any message that is not a command is sent to the AI agent (Claude or Gemini) as a conversation prompt. The AI operates within your active project's directory and can read files using safe, read-only tools.
Any message that is not a command is sent to the AI agent (Claude or Gemini) as a conversation prompt. The AI operates within your active project's directory. The tools available to the AI depend on the project's `permission_level`: `readonly` (default) restricts the AI to reading files, `read-write` allows file creation and editing, and `full` grants shell and network access. See [docs/security.md](docs/security.md) for details.

## 🔌 Plugin System

Expand Down Expand Up @@ -250,7 +254,7 @@ DevBridge is designed with multiple layers of security:

2. **Command Sandbox** -- The `/run` command only executes commands explicitly listed in the `commands` config. Commands are spawned without `shell: true` to prevent injection attacks.

3. **Read-Only AI Tools** -- The Claude adapter restricts the AI to read-only tools (`Read`, `Glob`, `Grep`), preventing the AI from modifying files when accessed via Telegram.
3. **Configurable AI Permission Levels** -- Each project has a `permission_level` (`readonly`, `read-write`, or `full`) that controls which tools the AI can use. The default `readonly` level restricts the AI to read-only tools (`Read`, `Glob`, `Grep`), preventing file modifications. Higher levels grant write or full access as needed.

4. **Webhook Signature Verification** -- GitHub webhooks are verified using HMAC-SHA256 signatures when a `secret` is configured.

Expand Down Expand Up @@ -307,7 +311,7 @@ curl -X POST http://localhost:9876/webhook/ci \
A: Claude CLI and Gemini CLI are supported out of the box. DevBridge uses an adapter system, so new CLIs can be added by implementing the `CLIAdapter` interface. See [docs/adapters.md](docs/adapters.md).

**Q: Can the AI modify my files?**
A: By default, no. The Claude adapter restricts the AI to read-only tools (`Read`, `Glob`, `Grep`). The Gemini adapter passes messages directly with no tool restrictions from DevBridge's side (restrictions depend on Gemini CLI's own configuration).
A: By default, no. Both adapters use a `permission_level` system that defaults to `readonly`, restricting the AI to read-only tools. Set `permission_level` to `"read-write"` to allow file creation and editing, or `"full"` for complete access including shell commands. See [docs/security.md](docs/security.md) for details.

**Q: Is it safe to expose the notification server to the internet?**
A: The server binds to `127.0.0.1` by default. If you need external access (e.g., for GitHub webhooks), use a reverse proxy with HTTPS, configure a webhook `secret` for HMAC verification, and consider firewall rules. Rate limiting is built in.
Expand Down
21 changes: 19 additions & 2 deletions devbridge.config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,27 @@
"path": "/absolute/path/to/your/project",
"adapter": "claude",
"model": "sonnet",
"description": "My main application"
"description": "Read-only (default) - can only read code"
},
"my-app-dev": {
"path": "/absolute/path/to/your/project",
"adapter": "claude",
"model": "sonnet",
"description": "Read + Write - can create and edit files",
"permission_level": "read-write"
},
"my-app-full": {
"path": "/absolute/path/to/your/project",
"adapter": "claude",
"model": "opus",
"description": "Full access - agents, commands, spec-driven dev",
"permission_level": "full"
},
"api-backend": {
"path": "/absolute/path/to/api",
"adapter": "gemini",
"description": "Backend API"
"description": "Backend API",
"permission_level": "full"
}
},
"commands": {
Expand All @@ -30,6 +45,8 @@
"defaults": {
"adapter": "claude",
"timeout": 120,
"stream_timeout": 3600,
"inactivity_timeout": 300,
"max_message_length": 4096,
"session_ttl_hours": 24,
"command_timeout": 60
Expand Down
68 changes: 52 additions & 16 deletions docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,30 @@ interface CLIAdapter {
// Check if the CLI binary is installed and accessible.
// Called at startup to detect available adapters.

chat(message: string, sessionId: string, options: ChatOptions & { cwd: string }): Promise<string>;
// Send a message to the AI and return the response text.
chat(
message: string,
sessionId: string | null,
options: ChatOptions & { cwd: string }
): Promise<ChatResult>;
// Send a message to the AI and return the response.
// - message: the user's text from Telegram
// - sessionId: unique identifier for the conversation session
// - sessionId: unique identifier for the conversation session (null for first message)
// - options.model: optional model name override
// - options.timeout: max seconds to wait (default: 120)
// - options.cwd: project directory to run the CLI in
// - options.allowedTools: optional comma-separated tool list override
// - options.skipPermissions: optional flag to skip interactive permission prompts

newSession(projectPath: string): string;
// Create a new session and return its ID.
// Called when the user starts a new conversation or after /clear.

clearSession(sessionId: string): void;
// Clean up resources associated with a session.
// Called when the user runs /clear or when sessions expire.
chatStream?(
message: string,
sessionId: string | null,
options: StreamChatOptions & { cwd: string }
): Promise<ChatResult>;
// Optional streaming variant of chat(). Sends output chunks incrementally
// via the onChunk callback as the CLI produces them.
// - options.onChunk: callback invoked with each output chunk
// - options.minChunkSize: minimum characters to buffer before calling onChunk
// - options.inactivityTimeout: seconds of no output before killing the process
}
```

Expand All @@ -47,6 +56,19 @@ type AdapterName = 'claude' | 'gemini'; // Extend this union for new adapters
interface ChatOptions {
model?: string;
timeout?: number;
allowedTools?: string; // Comma-separated list of tools (e.g., "Read,Glob,Grep")
skipPermissions?: boolean; // Skip interactive permission prompts in the CLI
}

interface ChatResult {
text: string;
sessionId: string | null;
}

interface StreamChatOptions extends ChatOptions {
onChunk: (chunk: string) => void | Promise<void>;
minChunkSize?: number;
inactivityTimeout?: number;
}
```

Expand Down Expand Up @@ -212,9 +234,13 @@ When the user runs `/clear` or a session expires:

---

## The `spawnCLI` Utility
## The `spawnCLI` and `spawnCLIStreaming` Utilities

DevBridge provides `src/utils/process.ts` with two functions for running CLI processes.

### `spawnCLI` -- Buffered execution

DevBridge provides `src/utils/process.ts` with a `spawnCLI` function that handles:
Runs a CLI command and returns the full output after the process exits. Best for short-lived commands.

- Process spawning with `spawn()` (no `shell: true`)
- Timeout management (SIGTERM followed by SIGKILL)
Expand All @@ -237,7 +263,15 @@ function spawnCLI(
): Promise<SpawnResult>;
```

Use this utility in your adapter to get consistent behavior with the rest of the system.
### `spawnCLIStreaming` -- Streaming execution

Runs a CLI command and streams output chunks as they arrive. Designed for long-running AI responses where you want incremental feedback.

- **Inactivity timeout**: If the process produces no stdout/stderr for `inactivityTimeout` seconds (default: 300), it is killed. This prevents hung processes from blocking indefinitely.
- **Hard timeout**: A safety-net `streamTimeout` (default: 3600 seconds / 1 hour) kills the process regardless of activity.
- Calls `onChunk` with each output chunk as it arrives

Use `spawnCLI` for quick operations (version checks, short prompts) and `spawnCLIStreaming` for AI chat responses that may take minutes.

---

Expand Down Expand Up @@ -269,8 +303,9 @@ class AdapterRegistry {

- **Binary**: `claude`
- **Session management**: Uses `--session-id` and `--resume` flags for multi-turn conversations
- **Output format**: `--output-format text`
- **Safety**: Restricts tools to `Read,Glob,Grep` via `--allowedTools`
- **Output format**: `--output-format stream-json` (streaming mode) or `--output-format text` (non-streaming)
- **Permission control**: Tools are configured via `--allowedTools` based on the project's `permission_level` (default: `Read,Glob,Grep`). When `skipPermissions` is enabled, passes `--dangerously-skip-permissions` to auto-approve tool usage
- **Streaming**: Supports `chatStream()` via `spawnCLIStreaming` with inactivity timeout
- **Session tracking**: An in-memory `Set<string>` tracks active sessions to decide when to use `--resume`
- **Error recovery**: Detects corrupted sessions from error messages and signals for automatic cleanup

Expand All @@ -279,7 +314,8 @@ class AdapterRegistry {
- **Binary**: `gemini`
- **Session management**: Uses `-p` flag for prompts (session continuity depends on Gemini CLI capabilities)
- **Output format**: Uses CLI defaults
- **Safety**: No DevBridge-side tool restrictions; relies on Gemini CLI's own safety configuration
- **Permission control**: Tools are configured via `--allowed-tools` based on the project's `permission_level`. When `skipPermissions` is enabled, passes `--yolo` to auto-approve tool usage
- **Streaming**: Supports `chatStream()` via `spawnCLIStreaming` with inactivity timeout

---

Expand Down
20 changes: 16 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ A map of named projects. Each project points to a local directory and specifies
| `adapter` | `"claude" \| "gemini"` | No | Value from `defaults.adapter` | Which AI CLI to use for this project |
| `model` | `string` | No | Value from `defaults.model` | Model name to pass to the AI CLI (e.g., `"sonnet"`, `"opus"`) |
| `description` | `string` | No | -- | Human-readable description shown in `/projects` output |
| `permission_level` | `"readonly" \| "read-write" \| "full"` | No | `"readonly"` | Controls which AI tools are available and whether permissions are auto-approved. See [security.md](security.md) for details |
| `allowed_tools` | `string` | No | Derived from `permission_level` | Explicit comma-separated list of allowed tools, overriding the permission level mapping. Advanced use only |
| `skip_permissions` | `boolean` | No | Derived from `permission_level` | If `true`, the AI CLI skips interactive permission prompts (Claude: `--dangerously-skip-permissions`, Gemini: `--yolo`). Advanced use only |

```json
{
Expand All @@ -63,7 +66,8 @@ A map of named projects. Each project points to a local directory and specifies
"path": "/home/user/projects/frontend",
"adapter": "claude",
"model": "sonnet",
"description": "React frontend app"
"description": "React frontend app",
"permission_level": "read-write"
},
"backend": {
"path": "/home/user/projects/backend",
Expand Down Expand Up @@ -170,7 +174,9 @@ Default values for various settings.
|--------|------|---------|-------------|
| `adapter` | `"claude" \| "gemini"` | `"claude"` | Default adapter when a project does not specify one |
| `model` | `string` | `undefined` | Default model passed to the AI CLI. If unset, the CLI's default is used |
| `timeout` | `number` | `120` | Maximum seconds to wait for an AI response before timing out |
| `timeout` | `number` | `120` | Maximum seconds to wait for an AI response before timing out (non-streaming mode) |
| `stream_timeout` | `number` | `3600` | Hard maximum seconds for a streaming AI response. Acts as a safety net |
| `inactivity_timeout` | `number` | `300` | Seconds of no stdout/stderr output before a streaming AI process is killed |
| `max_message_length` | `number` | `4096` | Maximum characters per Telegram message chunk. Responses exceeding this are split |
| `session_ttl_hours` | `number` | `24` | Hours of inactivity after which a session is automatically cleaned up |
| `command_timeout` | `number` | `60` | Maximum seconds for `/run` command execution |
Expand All @@ -181,6 +187,8 @@ Default values for various settings.
"adapter": "claude",
"model": "sonnet",
"timeout": 120,
"stream_timeout": 3600,
"inactivity_timeout": 300,
"max_message_length": 4096,
"session_ttl_hours": 24,
"command_timeout": 60
Expand Down Expand Up @@ -261,12 +269,14 @@ When the notification server is enabled, the following endpoints are available:
"path": "/home/user/projects/frontend",
"adapter": "claude",
"model": "sonnet",
"description": "React SPA"
"description": "React SPA",
"permission_level": "read-write"
},
"backend": {
"path": "/home/user/projects/backend",
"adapter": "gemini",
"description": "Go API"
"description": "Go API",
"permission_level": "readonly"
}
},
"commands": {
Expand All @@ -283,6 +293,8 @@ When the notification server is enabled, the following endpoints are available:
"defaults": {
"adapter": "claude",
"timeout": 120,
"stream_timeout": 3600,
"inactivity_timeout": 300,
"max_message_length": 4096,
"session_ttl_hours": 24,
"command_timeout": 60
Expand Down
54 changes: 33 additions & 21 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,32 +65,43 @@ Using `shell: true` would allow shell metacharacters (`|`, `;`, `&&`, `$()`, bac
- Avoid whitelisting commands that accept user input from arguments (the alias itself is the only input)
- Use specific commands rather than generic wrappers

### 3. Read-Only AI Tools
### 3. Configurable Permission Levels

**File**: `src/adapters/claude.ts`
**Files**: `src/adapters/permissions.ts`, `src/adapters/claude.ts`, `src/adapters/gemini.ts`

When DevBridge invokes Claude CLI, it restricts the AI to read-only file system tools:
Each project has a `permission_level` that controls which AI tools are available and whether interactive permission prompts are skipped. The default is `"readonly"`.

```typescript
const args = [
'-p', message,
'--session-id', sessionId,
'--output-format', 'text',
'--allowedTools', 'Read,Glob,Grep',
];
```
| Level | AI Can Do | Permissions Auto-Approved |
|-------|-----------|--------------------------|
| `readonly` (default) | Read files, search files and contents | No |
| `read-write` | All of the above + create, write, and edit files | Yes |
| `full` | All of the above + execute shell commands, access the network, run agents | Yes |

**Tool mapping per adapter**:

| Level | Claude CLI (`--allowedTools`) | Gemini CLI (`--allowed-tools`) |
|-------|------------------------------|-------------------------------|
| `readonly` | `Read,Glob,Grep` | `ReadFileTool,ReadManyFilesTool,GlobTool,GrepTool` |
| `read-write` | `Read,Glob,Grep,Write,Edit,MultiEdit` | Above + `WriteFileTool,EditTool` |
| `full` | Above + `Bash,WebFetch,WebSearch,Agent,NotebookEdit` | Above + `ShellTool,WebFetchTool,WebSearchTool` |

**What this means**:
- Claude can **read** files (`Read` tool)
- Claude can **search** for files (`Glob` tool)
- Claude can **search** file contents (`Grep` tool)
- Claude **cannot** write, edit, or delete files
- Claude **cannot** execute shell commands
- Claude **cannot** access the network
**Permission bypass flags**:
- `read-write` and `full` levels automatically pass `--dangerously-skip-permissions` (Claude) or `--yolo` (Gemini) to avoid interactive prompts that would block the CLI process
- The `readonly` level does not skip permissions since read-only tools do not trigger prompts

This ensures that even if the AI misinterprets a request, it cannot modify your codebase or execute arbitrary code.
**Advanced overrides**: You can bypass the permission level mapping entirely by setting `allowed_tools` (explicit tool list) or `skip_permissions` (explicit bypass flag) on a project. These take precedence over the `permission_level` defaults.

**Note for Gemini adapter**: The Gemini CLI adapter (`src/adapters/gemini.ts`) passes messages directly to the CLI without DevBridge-side tool restrictions. Any restrictions depend on Gemini CLI's own safety configuration.
```json
{
"projects": {
"my-app": {
"path": "/path/to/project",
"adapter": "claude",
"permission_level": "read-write"
}
}
}
```

### 4. Webhook Signature Verification

Expand Down Expand Up @@ -179,7 +190,7 @@ The `/mute` and `/notifications off` commands allow users to temporarily or perm
|--------|------------|
| Unauthorized Telegram user sends commands | Chat ID whitelist silently drops messages |
| Arbitrary command execution via `/run` | Command whitelist + no `shell: true` |
| AI modifies or deletes files | Claude restricted to Read/Glob/Grep tools |
| AI modifies or deletes files | Default `readonly` permission level restricts tools to Read/Glob/Grep. Higher levels (`read-write`, `full`) grant write or shell access intentionally |
| Forged GitHub webhooks | HMAC-SHA256 signature verification with timing-safe comparison |
| Webhook flood / DDoS | Per-IP rate limiting + local bind address |
| Config file exposure | `devbridge.config.json` is in `.gitignore` |
Expand All @@ -198,4 +209,5 @@ The `/mute` and `/notifications off` commands allow users to temporarily or perm
5. **Use a reverse proxy** -- If exposing the notification server externally, terminate TLS at the proxy
6. **Monitor logs** -- Check `~/.devbridge/logs/devbridge.log` for unauthorized access attempts
7. **Keep CLIs updated** -- Ensure Claude CLI, Gemini CLI, and `gh` CLI are up to date with the latest security patches
8. **Use `readonly` for projects that don't need write access** -- The default `readonly` permission level is the safest option. Only escalate to `read-write` or `full` for projects where the AI genuinely needs to create or modify files. Review your permission levels periodically
]]>
Loading
Loading