Security Model
Elftia implements a multi-layered defense-in-depth security architecture, covering the complete chain from Electron process isolation to AI tool call review.
Full Security Pipeline
The complete security pipeline for an external message (Channel) from input to execution:
flowchart TB
Input[Channel message input] --> Sanitize[InputSanitizer<br/>strip dangerous characters]
Sanitize --> Rate{RateLimiter<br/>rate check}
Rate -->|over limit| Reject1[Reject: 429]
Rate -->|pass| Permission{UserPermissionService<br/>user permissions}
Permission -->|blocked| Reject2[Reject: 403]
Permission -->|guest/moderator/admin| PG{PromptGuardian<br/>prompt injection detection}
PG -->|block mode + injection| Reject3[Reject + deflect message]
PG -->|monitor mode| Log1[Log event]
PG -->|pass| Magi[MagiService<br/>Agent processing]
Magi --> ToolCall[Tool call request]
ToolCall --> FW{ExecutionFirewall<br/>path deny list}
FW -->|deny| Block1[Deny: path blocked]
FW -->|pass| GA{GuardianAgent<br/>AI safety review}
GA -->|critical/high| Gate{ChannelPermissionGate<br/>human confirmation}
GA -->|low/none| Exec[Execute tool]
GA -->|monitor| LogExec[Log + execute]
Gate -->|approved| Exec
Gate -->|denied| Block2[Deny: user vetoed]
Exec --> Audit[AuditLogger<br/>audit record]
LogExec --> Exec
Log1 --> Magi
Electron Security Foundations
Context Isolation
Electron's Context Isolation ensures the renderer process cannot directly access Node.js APIs:
{/* Security configuration when creating BrowserWindow */}
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: preloadPath,
},
});
contextBridge
The preload script exposes only allowlisted APIs via contextBridge.exposeInMainWorld():
{/* preload/index.ts -- safely expose IPC interface */}
contextBridge.exposeInMainWorld('api', {
completion: {
chatInSession: (params) => ipcRenderer.invoke('completion:chat', params),
},
});
IPC Authentication Token
Every IPC call carries an authentication token; the main process validates it via secureHandle:
sequenceDiagram
participant R as Renderer
participant P as Preload
participant M as Main (secureHandle)
Note over R,M: App startup
M->>P: ipcRenderer.sendSync('auth:bootstrap') returns token
P->>P: Save token to memory
Note over R,M: IPC call
R->>P: window.api.someMethod(params)
P->>M: ipcRenderer.invoke(channel, {token, ...params})
M->>M: validateToken(token)
alt Token invalid
M-->>P: throw Error('AUTH_FAILED')
else Token valid
M->>M: execute handler
M-->>P: result
end
- Token generation: Main process generates a random token at startup
- Synchronous bootstrap: Preload retrieves the token via
sendSyncbefore the page loads - Per-call validation: All IPC calls wrapped with
secureHandlevalidate the token
ExecutionFirewall
Deterministic path access control that blocks access to system directories and credential files. Zero LLM overhead.
Deny Rules
{/* Path rule structure */}
interface DeniedPathRule {
pattern: RegExp;
reason: string;
denyOps?: ('read' | 'write')[];
}
| Category | Example Rules | Blocked Operations |
|---|---|---|
| System directories | C:\Windows\, /etc/, /usr/, /proc/ | read + write |
| Credential files | .ssh/, .aws/, .gnupg/, .env | read + write |
| Browser data | Chrome\User Data\, .mozilla/ | read + write |
| Registry | System32\config\SAM|SYSTEM|SOFTWARE | read + write |
| Config files | .gitconfig, .npmrc, .bashrc | write only |
Tool Path Extraction
Automatically extracts file paths from tool call parameters for checking:
{/* Tool-to-path-parameter mapping */}
const FILE_TOOL_PATH_PARAMS: Record<string, string[]> = {
Read: ['path', 'file_path'],
Write: ['path', 'file_path'],
Edit: ['path', 'file_path'],
ListDir: ['path', 'dir_path'],
};
{/* Firewall check interface */}
class ExecutionFirewall {
constructor(workspacePath: string);
checkPath(filePath: string, operation: 'read' | 'write'): FirewallCheckResult;
checkToolCall(toolName: string, toolInput: Record<string, unknown>): FirewallCheckResult;
}
interface FirewallCheckResult {
allowed: boolean;
deniedBy?: string;
reason?: string;
}
GuardianAgent
An LLM-based tool call safety reviewer, positioned after the ExecutionFirewall and before human confirmation.
Operating Modes
| Mode | Review Scope | Blocking Policy | Error/Timeout Behavior |
|---|---|---|---|
off | None | None | Does not run |
monitor | Sensitive tools | Log only, do not block | fail-open |
guard | Sensitive tools | Block high/critical | fail-open |
strict | All tools | Block medium+ | fail-closed |
Risk Levels
type RiskLevel = 'none' | 'low' | 'medium' | 'high' | 'critical';
| Level | Meaning | Example |
|---|---|---|
none | Completely safe | Reading a file within the workspace |
low | Minor risk | Writing to a project file |
medium | Moderate risk | Shell commands that modify state, installing packages |
high | High risk | Accessing sensitive paths, operating outside the workspace |
critical | Extremely dangerous | rm -rf, data exfiltration, privilege escalation |
Critical Rules
The following operations are always rated high or critical:
- File deletion outside the workspace
- Recursive delete commands (
rm -rf,del /s /q,Remove-Item -Recurse) - Access to system directories (
/etc,C:\Windows,C:\Users) - Privilege escalation (
sudo,runas) - Piping to shell (
curl | sh,wget | bash) - Credential/key access
Caching
Uses SHA-256 hashes to cache reviewed tool calls, avoiding redundant LLM calls:
{/* GuardianAgent cache key generation */}
function cacheKey(toolName: string, toolInput: unknown): string {
return createHash('sha256')
.update(JSON.stringify({ tool: toolName, input: toolInput }))
.digest('hex');
}
Sensitive Tool List
const SENSITIVE_TOOLS = new Set(['Bash', 'Write', 'Edit', 'Agent']);
Non-sensitive tools (e.g., Read, ListDir) skip review in monitor/guard mode; they are reviewed only in strict mode.
PromptGuardian
An LLM-based prompt injection detector used to detect malicious prompt injection in Channel messages:
Operating Modes
| Mode | Behavior |
|---|---|
off | Disabled |
monitor | Detect and log, do not block |
block | Block and return a deflection message when injection is detected |
Detection Mechanism
{/* PromptGuardian core interface */}
class PromptGuardian {
async review(content: string, source: MessageSource): Promise<{
allowed: boolean;
isInjection: boolean;
confidence: number;
reason: string;
cached: boolean;
}>;
}
- SHA-256 cache for already-reviewed content
- fail-open on timeout/error
- Enabled only for Channel-sourced messages
InputSanitizer
A regex-based sanitizer that removes dangerous characters from messages:
{/* Simplified InputSanitizer */}
class InputSanitizer {
sanitize(input: string): string;
updateConfig(config: SanitizationConfig): void;
}
Characters removed:
- Zero-width characters (ZWS, ZWNJ, ZWJ, ZWSP)
- Unicode bidirectional control characters
- Invisible formatting characters
- Control characters (newlines and tabs are preserved)
RateLimiter
A sliding-window rate limiter supporting per-user and global limits:
{/* Rate limiter configuration */}
interface RateLimitConfig {
perUser: {
maxRequests: number; // default 20
windowMs: number; // default 60000 (1 minute)
};
global: {
maxRequests: number; // default 100
windowMs: number; // default 60000
};
cleanupIntervalMs: number; // expired entry cleanup interval
}
class RateLimiter {
check(userId: string): { allowed: boolean; retryAfter?: number };
updateConfig(config: Partial<RateLimitConfig>): void;
}
UserPermissionService
Channel user permission management, supporting auto-registration and role assignment:
Role System
| Role | Permissions | Description |
|---|---|---|
admin | All | Administrator |
moderator | Send messages + trigger Agents | Moderator |
guest | Send messages (rate-limited) | Guest (default role) |
blocked | None | Banned user |
class UserPermissionService {
checkPermission(channelId: string, platformUserId: string): Promise<{
allowed: boolean;
role: UserRole;
user: ChannelUser;
}>;
autoRegister(channelId: string, platformUserId: string, displayName: string): Promise<ChannelUser>;
}
ChannelPermissionGate
Sensitive tool calls originating from a Channel require user confirmation:
sequenceDiagram
participant Agent as TinyElf Agent
participant Gate as PermissionGate
participant Channel as Channel Platform
participant User as Remote User
Agent->>Gate: requestPermission(toolCall)
Gate->>Gate: Generate confirmation fingerprint (SHA-256)
Gate->>Channel: Send confirmation message + fingerprint
Channel->>User: Agent wants to perform operation XX — reply YES to confirm
alt User confirms
User->>Channel: YES
Channel->>Gate: Match fingerprint
Gate-->>Agent: approved: true
else Timeout (60s)
Gate-->>Agent: approved: false, reason: timeout
else User denies
User->>Channel: NO
Gate-->>Agent: approved: false, reason: denied
end
AuditLogger
Security audit log that records all security-related events to the SQLite audit_log table:
Event Types
| Event Type | Description | Severity |
|---|---|---|
tool_executed | Tool call executed | info |
tool_blocked | Tool call blocked | warning |
tool_guardian_review | Guardian review result | info/warning |
permission_requested | Permission confirmation requested | info |
permission_granted | Permission approved | info |
permission_denied | Permission denied | warning |
rate_limited | Rate limit triggered | warning |
injection_detected | Prompt injection detected | critical |
user_blocked | User banned | warning |
{/* Audit log entry */}
interface AuditLogEntry {
id: string;
timestamp: number;
eventType: AuditEventType;
severity: 'info' | 'warning' | 'critical';
channelId?: string;
userId?: string;
toolName?: string;
details: Record<string, unknown>;
}
SecurityService / CryptoService
Application-level encryption services that protect sensitive data stored locally:
{/* Encryption scheme */}
class SecurityService {
encrypt(plaintext: string): string; // AES-256-GCM, IV + authTag + ciphertext
decrypt(ciphertext: string): string;
}
class CryptoService {
deriveKey(password: string, salt: Buffer): Buffer;
// PBKDF2, 100K iterations, SHA-512, 32 bytes (256 bits)
}
All API keys are encrypted via SecurityService.encrypt() before being stored in SQLite.
Related Files
| File | Description |
|---|---|
packages/desktop/app/main/services/platform/security/ExecutionFirewall.ts | Path firewall |
packages/desktop/app/main/services/platform/security/GuardianAgent.ts | AI tool review |
packages/desktop/app/main/services/platform/security/PromptGuardian.ts | Prompt injection detection |
packages/desktop/app/main/services/platform/security/RateLimiter.ts | Rate limiter |
packages/desktop/app/main/services/platform/security/InputSanitizer.ts | Input sanitizer |
packages/desktop/app/main/services/platform/security/UserPermissionService.ts | User permissions |
packages/desktop/app/main/services/platform/security/ChannelPermissionGate.ts | Permission confirmation gate |
packages/desktop/app/main/services/platform/security/AuditLogger.ts | Audit log |
packages/desktop/app/main/services/platform/security/SecurityService.ts | AES-256-GCM encryption |
packages/desktop/app/main/services/infra/crypto/CryptoService.ts | PBKDF2 key derivation |
packages/desktop/app/main/ipc/safe-handle.ts | IPC secure handle |
packages/desktop/app/preload/index.ts | contextBridge interface exposure |
packages/desktop/app/shared/contracts/security-types.ts | Security-related shared types |
packages/desktop/app/main/workers/db/auditLog.ts | Audit log DB operations |
packages/desktop/app/main/workers/db/channelUsers.ts | Channel user DB operations |