destreamed-runner
v0.3.1
Published
Headless autonomer Tuner-Runner für Destreamed — pollt Tasks, triggert wahlweise Claude Code oder Codex als LLM-Worker, mit Toolchain-Modulen (Rust/Python/Go/…), Vault-Integration und GitLab-Repo-Mirror.
Readme
destreamed-runner
⚠ Experimental — use at your own risk. Active development. Breaking changes likely. No SLA, no warranty. Don't run against credentials you can't rotate. Licensed under Apache 2.0.
Headless autonomer Tuner-Runner für Destreamed: pollt deinem Tuner zugewiesene Tasks, lässt einen LLM-Worker (Claude Code oder Codex) im Container die Arbeit erledigen, schreibt Echos + Complete via MCP zurück. Vault-Integration für Credentials on-demand, Auto-Update via git pull, modulare Toolchain-Module (Rust, Python, Go, kubectl, gh) per-Tuner pickbar.
Pattern: Dünner deterministischer Wrapper + LLM-CLI als Worker. Inspired by Ralph.
cron → flock → poll Destreamed → Vault-Inject → worker.spawn() → echo + complete via MCPQuick-Setup (init Wizard)
Sobald als npm-Paket published, ist der Bootstrap ein Einzeiler:
npx @destreamed/runner init
# (oder mit Bootstrap-Token aus der Destreamed-UI sobald DESTREAM-102 fertig:)
# npx @destreamed/runner init --token=brt_xxxIm Repo selbst (vor publish):
git clone [email protected]:destreamed/destreamed-cli.git ~/destreamed-cli
cd ~/destreamed-cli
npm install && npm run build
node bin/cli.js initWizard fragt der Reihe nach:
- Tuner-Name (default
peon) - DESTREAMED_AGENT_TOKEN (
ds_…) - Stream-Filter oder
allow_all - LLM-Worker (
claude-codedefault odercodex— siehe Worker) - Worker-Auth (Claude OAuth/API-Key oder Codex-Token, je nach Wahl)
- GitLab-PAT als optionaler
.env-Fallback (bevorzugt: Token im Vault, on-demand pro Task via${vault:Gitlab}) - Vault-Integration (KeyFile-Pfad mit Auto-Suggestion aus
~/Downloads/, Master-Passphrase, Persona-Pick aus dem KeyFile-Inhalt) - Toolchains (Multi-Select aus
rust,python,go,kubectl,gh— siehe Toolchains)
Schreibt .env atomar (mode 0600), kopiert KeyFile nach secrets/vault-keyfile.dskey, komponiert docker/Dockerfile.<tuner> aus Template + ausgewählten Module-Snippets, erstellt workspace/<tuner>/{tasks,scripts,artifacts}/-Skeleton, und zeigt am Ende einen OS-spezifischen Setup-Hint für Docker-Install + Container-Boot.
Dann:
./bin/tuner up <tuner-name>Installation auf macOS
Voraussetzungen:
- macOS (Apple Silicon oder Intel)
- Homebrew
- Funktionierender Destreamed-Account mit aktiver Vault-Persona ("Peon")
- Claude-Code-Subscription (Pro/Team)
- Optional: GitLab-Account mit PAT für Repo-Operationen
1. Dependencies via Homebrew
brew install git node colima docker docker-composecolima ist der schlanke OSS-Ersatz für Docker Desktop (Daemon in winziger Lima-VM, free, keine Lizenz-Stress). Wer Docker Desktop schon installiert hat, kann das auch nehmen — colima ist nicht zwingend.
2. Docker-Daemon starten
colima start
docker version # sollte Server-Version anzeigen3. Repo klonen
git clone [email protected]:destreamed/destreamed-cli.git ~/destreamed-cli
cd ~/destreamed-cli(SSH-Key braucht Zugriff auf gitlab.cco.re. Alternativ HTTPS-Klon mit PAT.)
4. Tokens besorgen — drei Stück
| Token | Wo holen | Beispiel |
|---|---|---|
| Destreamed-Tuner-Token | destreamed.com → Profile → Tuners → New Agent → Token einmalig kopieren | ds_… |
| Claude-Code-OAuth-Token | claude.ai → Profile → Headless / CI Token → kopieren | oat_… oder sk-ant-oat-… |
| GitLab-PAT (optional) | gitlab.cco.re → User Settings → Access Tokens → Scopes api, read_repository, write_repository → kopieren | glpat-… |
5. Vault-KeyFile besorgen
In der Destreamed-Browser-Extension:
- Vault-Bereich → Tuner-Identität für „Peon" anlegen (oder bestehende auswählen)
- KeyFile exportieren — landet als
~/Downloads/destreamed-vault-YYYY-MM-DD.dskey
Datei ins Repo legen + ablegen:
mkdir -p secrets
cp ~/Downloads/destreamed-vault-*.dskey secrets/vault-keyfile.dskeyDie Datei ist Argon2id-encrypted, du brauchst die Master-Passphrase die du beim Browser-Vault-Setup vergeben hast.
6. .env befüllen
cp .env.example .env
$EDITOR .envMindest-Werte:
DESTREAMED_AGENT_TOKEN=ds_xxxxx # aus Schritt 4
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat-xxx # aus Schritt 4
GITLAB_TOKEN=glpat-xxx # optional
TUNER_VAULT_PASSPHRASE=deine-passphrase # aus Schritt 5
TUNER_VAULT_AGENT_ID=697352f5-… # die UUID deiner Persona; bin/tuner enroll listet sie
TUNER_NAME=peon # default ist peon
TUNER_VAULT_KEYFILE_PATH=/Users/$USER/destreamed-cli/secrets/vault-keyfile.dskeyDie Persona-UUID rauszufinden:
npm install
npx tsx src/cli.ts vault enroll
# → listet alle Personas im KeyFile mit ihrer UUID
# trage die gewünschte UUID in .env als TUNER_VAULT_AGENT_ID ein7. Container starten
./bin/tuner up peon
./bin/tuner logs peon # Logs streamen (Ctrl+C beendet das Streamen, nicht den Container)Erste Tick-Ausgabe nach max 2min. Bei korrekt befülltem .env: {"l":"info","msg":"no_tasks"} (sofern der Tuner gerade nichts zu tun hat).
8. Verifikation
In Destreamed:
- Stream "Sandbox" o.ä. anlegen, Peon als Tuner adden
- Task droppen: „Echo
hello worldzurück und complete den Task" - Innerhalb von 2 min sollte ein Echo + Complete erscheinen
Wenn ja: läuft. Wenn nein: → Troubleshooting unten.
9. Auto-Update einrichten (optional)
crontab -e
# Folgende Zeile hinzufügen:
*/15 * * * * /Users/$USER/destreamed-cli/bin/auto-update.shDas Skript checkt alle 15 min ob ein neuer Commit auf origin/main liegt; wenn ja: pull + Container-Rebuild. Logs landen in ~/destreamed-cli/auto-update.log.
ENV-Overrides möglich (alle optional):
DCLI_BRANCH=main— welcher BranchDCLI_TUNER=peon— welcher Tuner-NameDCLI_LOG=/var/log/dcli.log— Log-Pfad
Architektur
| Komponente | Rolle |
|---|---|
| src/cli.ts | Entry: tick, check, vault {enroll,whoami,list,get}, git mirror, echo, complete, search, read |
| src/vault/{crypto,keystore,client,inject,runtime}.ts | Vault-Crypto + REST-Client + Placeholder-Inject |
| src/destreamed/client.ts | MCP-Client für Destreamed (tasks, beat, stream, search) |
| src/git-mirror.ts | GitLab-Group-Mirror in workspace |
| src/prompt-builder.ts | Token-bewusster Prompt für worker.spawn() |
| src/workers/{types,claude-code,codex,registry}.ts | Worker-Abstraction — austauschbarer LLM-Subprocess |
| src/dockerfile-composer.ts | Bastelt docker/Dockerfile.<tuner> aus Template + Module-Snippets |
| docker/Dockerfile.template | Base-Image mit {{TOOLCHAIN_MODULES}} Placeholder |
| docker/modules/<name>.dockerfile | Toolchain-Snippets (rust, python, go, kubectl, gh) |
| docker/{tick,entrypoint}.sh | Cron-Wrapper |
| bin/tuner | Multi-Tuner-Helper (add/list/up/down/logs) — komponiert Dockerfile bei up |
| bin/auto-update.sh | Host-side updater (git pull + rebuild) |
Ein-Tick-Loop (deterministisch, kein LLM für die Steuerung):
flock— single-flighttasks.get_my_tasksvia MCP über Destreamedstream.get_memosfür aktiven Stream- Bei Repo-Hint im Task:
git cloneins Per-Task-Workspace ${vault:NAME}-Platzhalter im Task scannen → Vault holt Plaintexts →extraEnvfür Worker- Worker laden (
workers/registry.ts) + Auth-Detect aus ENV - MCP-Config schreiben (nur wenn Worker
mcpSupported; bei Codex skipped) worker.spawn()mit Prompt + ENV — kapseltclaude -presp.codex exec- Bei Exit ≠ 0: Fehler-Echo. Bei Exit 0: Logs (Worker erledigt echo+complete selbst — bei Codex via Bash-Subcommands
destreamed-cli echo|complete|search|read)
Worker
RUNNER_WORKER in .env wählt den LLM-Subprocess:
| Worker | ENV vars | MCP | Container-Install |
|---|---|---|---|
| claude-code (default) | CLAUDE_CODE_OAUTH_TOKEN (oat_/sk-ant-oat) oder ANTHROPIC_API_KEY | ✓ nativ | im Base-Image |
| codex | CODEX_AUTH_TOKEN | ✗ — Beat-Ops via Bash-Subcommands | Toolchain-Modul codex (npm @openai/codex) |
destreamed-cli check zeigt aktuell gewählten Worker + Auth-Status. Wechsel: RUNNER_WORKER=… editieren, Container neu starten (bin/tuner up <name>).
Toolchain-Module
Per-Tuner Container-Build aus modularen Snippets:
docker/
├── Dockerfile.template # Base + {{TOOLCHAIN_MODULES}} Placeholder
└── modules/
├── rust.dockerfile # ~400 MB
├── python.dockerfile # ~150 MB (Python 3 + uv)
├── go.dockerfile # ~300 MB
├── kubectl.dockerfile # ~80 MB (kubectl + helm)
├── gh.dockerfile # ~30 MB (GitHub CLI)
└── codex.dockerfile # ~50 MB (OpenAI Codex CLI — für RUNNER_WORKER=codex)Selection in .env (komma-separiert):
RUNNER_TOOLCHAINS=rust,pythonbin/tuner up <name> ruft den Composer auf bevor compose läuft → schreibt docker/Dockerfile.<name>. Per-Tuner Image, gemeinsamer Layer-Cache wenn Selection identisch ist. Manuell: destreamed-cli compose-dockerfile --tuner peon --toolchains rust,python.
Eigenes Modul anlegen: neue Datei docker/modules/<name>.dockerfile mit RUN/ENV-Snippets (kein FROM!). Das Snippet darf USER root setzen, sollte aber mit USER peon enden.
Workspace-Layout
Bind-Mount zwischen Host und Container, per-Tuner gescoped:
host: container sieht:
./workspace/peon/ /workspace/
├── tasks/<task-id>/ ├── tasks/<task-id>/ ← per-task scratch
├── scripts/ ├── scripts/ ← User → Container
├── artifacts/ ├── artifacts/ ← Container → User
└── repos/destreamed/{frontend,…}/ └── repos/ ← git mirror outputscripts/— User-managed, Container darf executenartifacts/— Container schreibt Output, User holt abrepos/— wird von Tasks befüllt (z.B. viadestreamed-cli git mirror <ns>als ad-hoc-Befehl)tasks/<id>/— Per-Task-Scratch, enthält generierte.mcp.json
Tuner B sieht NICHT in Tuner A's workspace — der Mount ist physisch auf workspace/<TUNER_NAME>/ gescoped.
Multi-Tuner
bin/tuner add <name> # neuer Tuner: <name>.env + workspace/<name>/
bin/tuner list # alle mit Run-Status
bin/tuner up <name> # starten (compose --env-file --project-name up -d --build)
bin/tuner down <name> # stoppen
bin/tuner logs <name> # logs -fVault-KeyFile ist user-global (alle Personas in einem File secrets/vault-keyfile.dskey). Jeder Tuner pickt seine Persona via TUNER_VAULT_AGENT_ID in seinem <name>.env. Beim Hinzufügen einer neuen Persona: im Browser enrollen → KeyFile re-exportieren → secrets/vault-keyfile.dskey ersetzen. Container brauchen keinen Restart (loadVaultAgent liest File bei jedem Aufruf neu).
Manuell statt Helper:
docker compose --env-file scout.env --project-name scout up -d --buildVault-Integration
Lese-Subcommands (ohne MCP-Overhead):
destreamed-cli vault enroll # listet Personas im KeyFile
destreamed-cli vault whoami # bestätigt Server-Identität
destreamed-cli vault list # alle gegrant'eten Credentials
destreamed-cli vault get <name> # plaintext auf stdoutAuto-Inject im Tick: Tasks/Memos können ${vault:NAME} enthalten. Beim Tick werden die Werte einmal aus dem Vault gezogen und als ENV in den claude-Subprocess gepusht ($NAME). Plaintext lebt nur für die Subprocess-Lebensdauer.
# Beispiel-Task in Destreamed:
"Verbinde dich mit GitLab via ${vault:Gitlab} als Token. Liste meine Repos."
# Wird im Tick zu:
"Verbinde dich mit GitLab via $GITLAB als Token. Liste meine Repos."
+ ENV={GITLAB: "<plaintext>"}Hard-fail wenn Credential nicht resolvable (403/missing) — Echo erklärt was fehlt, Task bleibt offen.
GitLab-Repo-Mirror (optional, Power-User)
Manuelles Subcommand — KEINE automatische Cron-Synchronisation. Wird über Vault-Cred Gitlab authentifiziert:
destreamed-cli git mirror destreamed # alle destreamed/*-Repos
destreamed-cli git mirror destreamed --dest /tmp/foo # eigener PfadOutput landet in workspace/<tuner>/repos/<group>/<project>/. Für regelmäßige Sync: drop einen Task in Destreamed, der das Subcommand aufruft — der Runner führt's aus, kein eingebauter Cron.
Token-Modell
| Token | Wer nutzt | Wofür |
|---|---|---|
| DESTREAMED_AGENT_TOKEN (ds_…) | CLI | MCP-Polling, Tasks holen, Echos, Complete |
| CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY | claude-code Worker | LLM-Calls an Anthropic (Format auto-detected) |
| CODEX_AUTH_TOKEN | codex Worker | LLM-Calls an OpenAI Codex |
| TUNER_VAULT_PASSPHRASE | CLI | Master-Passphrase zum Entschlüsseln des KeyFiles (Argon2id) |
| GITLAB_TOKEN (in .env, optional) | CLI für git clone initial | nur wenn Repo-Tasks ohne Vault gewünscht |
| Vault-credential „Gitlab" | dynamisch geholt von CLI + Worker | bevorzugte Methode für GitLab-Auth — rotation ohne .env-Touch |
Operationelle Notizen
- Logs:
bin/tuner logs peon(NDJSON). - Cron-Intervalle:
*/2 tick,5 */6 mirror. Anpassen indocker/crontab+ rebuild. - Stale Lock: Bei Crash bleibt
/tmp/destreamed-cli.lockim Container liegen; Container-Restart räumt auf. PID-Detection ist Phase-2-Feature. - Failure-Mode:
claude -pfailed → Fehler-Echo, Task NICHT completed → nächster Tick versucht erneut. Vorsicht bei nicht-idempotenten Side-Effects. - Workspace-Cleanup: Aktuell keiner; Volume wächst.
tick --gcist Folge-Iteration. - Token-Mask: Mirror-Errors mask
oauth2:<token>@zuoauth2:***@. Andere Code-Pfade noch nicht gehärtet — siehe Security-Sektion.
Troubleshooting
config_invalid: TUNER_STREAM_FILTER fehlt
Du hast keinen Filter UND kein TUNER_ALLOW_ALL_STREAMS=true. Setze einen Stream-ID-Filter oder erlaube explizit alle Streams.
vault GET /vault/me 401
- KeyFile-Persona auf Server nicht enrolled (DB hat keinen
vault_pubkey_signfür die UUID) - Browser-Vault zeigt vermutlich auf eine andere Server-Instanz (lokal vs production)
- Re-Enroll im Browser → KeyFile re-exportieren
Cron-Job loggt nichts:
crontab -u peonmuss installiert sein → check viadocker exec peon-tuner crontab -l -u peon- ENV nicht in
/etc/destreamed-runtime.env→ entrypoint hat sie nicht gefroren, check Container-Logs
fatal: detected dubious ownership
- safe.directory greift nicht — Dockerfile-Step fehlt oder Container nicht rebuildet (
bin/tuner up peonmit--build)
colima vs Docker Desktop:
- Beide funktionieren. Bei Wechsel:
~/.docker/config.jsoncredsStore-Zeile prüfen (Docker-Desktop hinterlässtosxkeychain-Verweis, der bei colima failed)
Tokens leak in Logs:
- Aktuell teilweise gemasked (Mirror-Errors), aber NICHT systematisch. Im Logger registrierter Sanitizer ist Phase-2-Feature.
Aktueller Status
✅ Phase 1: deterministischer Tick mit Lock, Vault-Integration, Repo-Mirror, Multi-Tuner-Isolation, Auto-Update ✅ Phase B (v0.3.0): Modulare Toolchain-Module (Rust/Python/Go/kubectl/gh), per-Tuner Dockerfile-Composition ✅ Phase C (v0.3.0): Worker-Abstraction — Claude Code / Codex pickbar, Worker-Interface für künftige Anbieter ✅ v0.3.1: Codex-Toolchain-Modul + worker-aware Prompt (Codex sieht keine MCP-Tools mehr, nur Bash-Subcommands) ⚠️ Stale-Lock-PID-Detection fehlt ⚠️ Workspace-Garbage-Collection fehlt ⚠️ Image-basierter Production-Update (Pattern C mit Watchtower) ist Folge-Iteration
Lizenz
Internal — kein Public-Use, no warranty.
