githubinferredactive
creel
provenance:github:Creel-ai/creel
Secure, self-hosted personal AI agent with per-tool container isolation
README
# Creel
<p align="center">
<img src="assets/creel-logo.jpg" alt="Creel" width="400">
</p>
A secure LLM task runner and personal AI assistant that separates credential-bearing data fetching from LLM processing. Supports both scheduled tasks (morning briefings, weather summaries) and interactive agent mode (chat via CLI or iMessage with tool calling).
A creel is a wicker basket usually used for carrying fish or blocks of peat. It is also the fish trap used to catch lobsters and other crustaceans.
## Why
Agentic LLM systems give the model access to tools, credentials, and untrusted input all at once. Creel enforces **per-tool isolation** — each executor runs in its own container with only the credentials it needs:
| Component | Has access to | Does NOT have |
|-----------|--------------|---------------|
| Each executor | Only its own credential (one OAuth scope, one API key) | LLM, other tools' credentials |
| Bridge executors | Scoped HTTP token for one macOS app | LLM, other bridge endpoints |
| LLM Runner | Anthropic API key only | Any tool credentials |
| Orchestrator | All secrets, LLM output | Untrusted external input |
Even if prompt injection occurs (e.g., via a calendar event title), the LLM container has nothing to exfiltrate except its own API key. A compromised executor can only access its one scoped credential — not your email, not your files, not your messages.
## Architecture
```mermaid
flowchart TD
subgraph orch["Orchestrator"]
direction TB
schedule["Scheduler / Agent Loop"]
guardian["Guardian Pipeline"]
template["Prompt Builder"]
output["Output Router"]
end
subgraph containers["Docker Executors (isolated)"]
direction TB
google["Google Suite\n📅 Calendar · 📧 Gmail · 📁 Drive"]
web["Web Tools\n🔍 Search · 🌐 Fetch · 🌤 Weather"]
exec["Shell / Exec\n⚙️ Mounted paths only"]
end
subgraph bridge["Host Bridge (macOS native)"]
direction TB
bridge_api["FastAPI Server"]
native["📝 Notes · ✅ Reminders\n📋 Things 3 · 💬 iMessage"]
end
subgraph llm_container["LLM Container"]
llm["Claude\n🔑 Anthropic API key only"]
end
subgraph channels["Channels"]
cli["TUI / CLI"]
imsg["iMessage"]
end
channels -- "message" --> orch
schedule -- "tool call" --> guardian
guardian -- "approved" --> containers
guardian -- "approved" --> bridge_api
bridge_api --> native
containers -- "JSON result" --> template
bridge_api -- "JSON result" --> template
template -- "prompt\n(no secrets)" --> llm
llm -- "response" --> output
output --> channels
style containers fill:#2d333b,stroke:#f47067,stroke-width:2px,color:#f0f0f0
style bridge fill:#2d333b,stroke:#fd7e14,stroke-width:2px,color:#f0f0f0
style llm_container fill:#2d333b,stroke:#f47067,stroke-width:2px,color:#f0f0f0
style orch fill:#2d333b,stroke:#58a6ff,stroke-width:2px,color:#f0f0f0
style channels fill:#2d333b,stroke:#3fb950,stroke-width:2px,color:#f0f0f0
```
> **Key insight:** Each red box is a separate security boundary. The LLM never sees credentials. Executors only get their own scoped secret. Even a compromised tool can't reach other tools' data.
### Agent Mode
In agent mode, the same security boundary applies — the LLM requests tool calls, but the orchestrator handles secrets injection and executor execution:
```mermaid
flowchart TD
CH["Channels\nstdin | iMsg | BB"] -- "incoming message" --> SM["Session Manager\n(JSON files)"]
SM -- "message + history" --> AL["Agent Loop"]
AL --> LLM["LLM call"]
LLM --> TU{"tool_use?"}
TU -- no --> resp["Response"]
TU -- yes --> EX["Execute via executor\n(secrets injected)"]
EX --> TR["tool_result"] --> AL
```
Scheduled tasks can also use agent mode by setting `mode: agent` in the task YAML.
### Guardian
The guardian layer screens inputs and validates tool calls before they execute. All stages are optional and independently configurable in `agent.yaml`:
```mermaid
flowchart TD
A["Incoming message"] --> B["screen_input(text)\n← before session history"]
B --> FC["FastClassifier\nDeBERTa/ONNX, ~10ms"]
B --> LJ["LLMJudge\nHaiku, ~300ms, off by default"]
FC --> blocked{"blocked?"}
LJ --> blocked
blocked -- yes --> reject["Return rejection,\nskip agent loop"]
blocked -- no --> agent["Agent loop → LLM returns tool_use"]
agent --> VA["validate_action(tool, args)\n← before execute_tool_call"]
VA --> PE["PolicyEngine\nYAML rules, <1ms"]
VA --> CC["CoherenceCheck\nHaiku, ~300ms"]
PE -- allow --> execute["Execute"]
PE -- review --> approval["Human approval\nor auto_approve"]
PE -- deny --> err1["Return error to LLM"]
CC -- coherent --> execute
CC -- incoherent --> err2["Return error to LLM"]
```
**Stages:**
| Stage | Component | What it does |
|-------|-----------|--------------|
| 1 | Fast classifier | Local DeBERTa model scores prompt-injection likelihood against a confidence threshold |
| 2 | LLM judge | Secondary Haiku-based check (disabled by default) |
| 3 | Policy engine | `fnmatch` rules in `policies/default.yaml` map tool names to allow/review/deny |
| 4 | Coherence check | LLM-based check that tool calls match the user's original intent (catches prompt injection causing unrelated actions) |
**Policy rules** (`policies/default.yaml`):
```yaml
allow: [check_weather, check_calendar, check_email, read_email, check_drive, check_messages, get_chats, react_imessage]
review: [send_*, upload_*, create_*, mark_*, trash_*]
deny: [delete_*]
# Tools that match 'review' rules but can skip human approval
auto_approve:
- mark_read
- react_imessage
```
Deny wins over review, review wins over allow. Unknown tools default to review. Tools listed in `auto_approve` skip the human confirmation prompt even when matched by a review rule.
**Human-in-the-loop review:** Tools matching `review` rules prompt the user for approval before executing. In the TUI, this appears as an inline confirmation dialog. A configurable timeout (default 60s) denies the action if no response is received.
**Audit logging** writes to `guardian_audit.jsonl` with hashed inputs (never raw text), tool names, arg keys (not values), verdicts, and confidence scores.
**Configuration** in `agent.yaml`:
```yaml
guardian:
enabled: true
review:
timeout_seconds: 60
default_on_timeout: deny
fast_classifier:
enabled: true
threshold: 0.95
model_name: protectai/deberta-v3-base-prompt-injection-v2
llm_judge:
enabled: false
coherence:
enabled: true
model: claude-haiku-4-5-20251001
max_tokens: 256
policy:
enabled: true
policy_file: policies/default.yaml
audit:
enabled: true
log_file: guardian_audit.jsonl
```
## Host Bridge
For macOS-specific tools (Apple Notes, Apple Reminders, Things 3, iMessage), Docker containers can't directly execute AppleScript or access macOS applications. The host bridge solves this by running a FastAPI server on the host system that provides authenticated HTTP endpoints for containerized executors.
**Why**: Docker containers are sandboxed and can't access the macOS scripting bridge or application APIs that tools like Notes, Reminders, and Things 3 require.
**How**: A FastAPI server (`python -m bridge.server`) runs as a host process and exposes REST endpoints at `/notes/*`, `/reminders/*`, `/things/*`, and `/imessage/*`. Containerized executors make HTTP requests to these endpoints with scoped authentication tokens.
**Security**: Each executor receives a scoped token that only grants access to its specific tool endpoints. For example, the `apple_notes` executor can only call `/notes/*` endpoints, not `/reminders/*` or `/things/*`.
**CLI Integration**: The bridge server delegates to command-line tools:
- **Apple Notes**: `memo` CLI for reading/writing notes
- **Apple Reminders**: `remindctl` CLI for managing rem
[truncated…]PUBLIC HISTORY
First discoveredMar 23, 2026
IDENTITY
inferred
Identity inferred from code signals. No PROVENANCE.yml found.
Is this yours? Claim it →METADATA
platformgithub
first seenFeb 5, 2026
last updatedMar 22, 2026
last crawled17 days ago
version—
README BADGE
Add to your README:
