frederico-kluser/dsh-guard-messenger ↗★ 0
dsh-guard-messenger
Plugin Cordis para o DeepSeek Harness: acesso remoto pelo Telegram sem login — local abre direto, túnel autenticado por chave no link (?key=), /rotacionar revoga chave, sessões e conexões ativas; controle do túnel e do bot na aba "Remote Access" do modal de settings do DSH. 适合需要通过手机Telegram远程安全访问和控制DSH的用户。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:frederico-kluser/dsh-guard-messengerdsh-guard-messenger

Usa o teu próprio DeepSeek Harness pelo celular — a Web UI inteira, para codificar de verdade — sem nunca alargar o bind para fora do loopback: o túnel termina em 127.0.0.1 (acesso local abre direto), e pelo túnel só entra quem tem a chave no link ?key= (que o bot envia) — ligas e desligas o acesso pelo Telegram.

Instalação (uma linha)
dsh plugin --profile web add dsh-guard-messenger
Instalação pelo link do repositório
dsh plugin add github:frederico-kluser/dsh-guard-messenger
Funciona sem allowBuilds e sem build no install: os artefactos compilados
(dist/, lib/) vêm commitados no git e os hooks prepare/prepack foram
removidos de propósito — o pnpm 11 bloqueia build de dependências git
(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED), que era exactamente o que impedia o
install-by-link.
Nota de release: o comando só funciona depois de este ramo estar pushado no GitHub. Enquanto o repositório servir a árvore antiga (a que ainda tinha
prepare), opnpm add github:…falha comERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED.
Instalação por tarball: os artefactos compilados (
lib/client.jsedist/) vêm commitados no repositório (o que faz o install-by-link funcionar semallowBuilds) eprepublishOnlycontinua como hook de release. Sem esses artefactos a ativação do client lançariaMissingClientBundleError. Verdocs/PANEL-TELEGRAM.md.
Modelo de ameaça, em 5 linhas — antes de qualquer feature
Este plugin expõe, por escolha, um agente que executa código na tua máquina. Antes de continuares, lê isto:
- O TLS termina na borda da Cloudflare. O texto claro (prompts, código, respostas) passa por um terceiro — é o que permite WAF/Access/cache. Não é ponta-a-ponta.
- A URL do túnel é pública e não é segredo. Quem protege é a chave
?key=no link, não a obscuridade do endereço.- Cloudflare Access não pode ficar na frente de um quick tunnel. Sobre
*.trycloudflare.comtoda a autenticação tem de estar dentro da aplicação.- O quick tunnel não tem SLA — é "intended for testing and development only".
- Isto não é "seguro por padrão sem pensar". Reduzimos a superfície, autenticámos a borda por chave/sessão e demos-te o botão de desligar; não eliminámos a categoria do risco.
Detalhe e o que cada mitigação faz:
docs/THREAT-MODEL.md.
Modelo de segurança (o que o código garante de facto)
Leitura honesta das garantias — cada linha aponta para o código que a cumpre; nada aqui é "seguro por padrão sem pensar", é redução de superfície e autenticação.
| Propriedade | Como |
|---|---|
| O bind nunca é alargado | assertSecureBind recusa 0.0.0.0/:: no carregamento, com falha ruidosa (src/config/bind.ts) |
| Acesso local abre direto | em 127.0.0.1 o DSH responde sem barreira; a proteção aplica-se à superfície do túnel, não ao loopback (src/tunnel/proxy.ts) |
| Pelo túnel só entra com sessão ou chave no link | o proxy autentica tudo: sessão (cookie) ou ?key= no link; fora disso → 401 sem desafio (src/http/gate.ts, src/session/link-token.ts) |
| Origem e Host vêm primeiro | trustedRemotes (403 sem credencial) e Host byte-a-byte contra DNS rebinding (src/http/gate.ts:15, src/http/host-header.ts) |
| Chave no link reutilizável, revogável por rotação | CSPRNG 256 bits; guardada só como digest SHA-256; reutilizável até /rotacionar (gera chave nova, invalida sessões e encerra as conexões ativas — WebSockets e streams em voo caem na hora) ou derrubar o túnel (src/session/link-token.ts, src/tunnel/proxy.ts) |
| O 401 é sem desafio | texto puro, sem desafio de login (o popup do navegador foi removido) — nunca se pede senha em prompt/formulário (src/http/responses.ts, denyUnauthorized) |
| Força bruta tem teto | a 5ª falha começa a atrasar; 100 falhas acumuladas derrubam a exposição (modo restrito), só o loopback passa e o reiniciar não o contorna (src/ratelimit/**) |
| Verificação da chave em tempo constante | digest comparado com timingSafeEqual; o token redige-se em JSON/inspect (src/session/link-token.ts, test/security/timing-constante.test.ts) |
danger-full-access vetado | elevação proibida recusada como defesa em profundidade (src/permissions/deny.ts) |
| Só o dono pareado comanda o bot | allowlist de dois eixos (userKey/chatKey), default deny (worker/surface/auth.ts) |
| O que NÃO se garante | o TLS termina na borda da Cloudflare (texto claro passa por lá); a URL do túnel não é segredo; quem tiver o link acede até rotacionar; a ?key= viaja em query (visível a intermediários) — trade assumido pelo dono; prompt injection continua aceite — decisões de desenho em docs/THREAT-MODEL.md §4 e §5.1 |
A tensão central, dita por nós antes que digam por nós
O projeto foi construído para travar o DSH em loopback. Este plugin também o expõe pela internet. Parece contradição — e a formulação honesta é esta:
O bind continua em
127.0.0.1e o DSH aí abre direto (sem login). O que muda é que passa a existir um processo filho supervisionado (cloudflared) que leva o tráfego da borda da Cloudflare até um proxy dedicado que autentica na borda (sessão ou?key=). Não é o mesmo que--host 0.0.0.0: o socket local nunca é alargado, a exposição é opt-in, efémera e revogável em um comando — e quem proteger/api, o fallback da SPA e o handshake de WebSocket é esse proxy, não o loopback.O que não muda: a superfície de ataque lógica cresce. Antes, um atacante precisava de acesso à máquina; agora, pelo túnel, precisa da chave no link (ou de uma sessão roubada). Trocámos "inalcançável" por "alcançável e autenticado na borda" — sem login, sem senha a digitar. Essa troca é reversível a qualquer momento pelo botão de desligar (na aba "Remote Access" das settings do DSH, ou
/desligarno bot; a chave, pelo/rotacionar).
Quem não aceitar esta troca deve usar Tailscale ou SSH — e dizemo-lo com mais calma em docs/TUNNEL.md e na secção "Quando NÃO usar" abaixo.
O que faz (e porquê)
- Protege o túnel, não o loopback. O DSH abre direto em
127.0.0.1(sem login); quem expõe é um proxy dedicado que autentica tudo o que chega da internet —/api, o fallback da SPA e o handshake de WebSocket. Recusa endereços de bind fora do loopback no carregamento e recusa permissões proibidas (danger-full-access). Resolve a superfície da discussão upstream #853. - Nunca pede senha a ninguém. O acesso pelo túnel entra por sessão ou pela chave no link
?key=(CSPRNG, 256 bits, digest em disco). A chave é reutilizável até/rotacionar(que gera chave nova, invalida sessões e encerra ativamente as conexões já abertas — quem tiver o link antigo cai na hora, incluindo WebSockets) ou derrubar o túnel. O 401 é sem desafio de login — não há prompt nem formulário de login. - Suba um túnel efémero para acederes pelo celular, com TTL que o derruba sozinho e um probe fail-closed que impede um túnel "nu" (sem proxy autenticado atrás).
- Ligar/desligar pelo Telegram ou pela aba "Remote Access" das settings do DSH — o botão de matar na mão.
- Dispara agentes do harness pelo bot — com uma skill da allowlist e um prompt, o dono manda um subagente do DeepSeek Harness trabalhar na própria máquina (
/agente), acompanha os runs (/agentes) e cancela (/parar-agente). O dispatch executa código no host: exige confirmação em duas etapas, e o agente corre com as permissões do harness — nunca com o token do bot. Manual completo:docs/AGENTS.md.
Bot e túnel: a aba "Remote Access" das settings do DSH
O controle do bot e do túnel vive DENTRO da UI do DSH — na aba "Remote Access"
do modal de settings (o plugin registra-se no slot settings.section; não há
bloco injetado na home). A aba traz o chip de estado do bot OFFLINE/ONLINE
fiel ao runtime, o formulário de token, o pareamento pelo painel, a privacidade e
o cartão "Túnel" — estado ao vivo (desligado/ligando/online/instável/
desligando/falhou), a URL quando READY, o countdown "expira em", as tentativas
e os botões Ligar túnel (confirmação em duas etapas), Desligar túnel
(confirmação) e Repor (após falha) (duas etapas, só quando falhou).
O botão "Ver instruções" ("Como ligar o bot") pede ao servidor os passos do provedor ativo:
- OFFLINE → as instruções de conexão (Telegram: criar o bot no
@BotFather),dsh-guard-setup --pedir-token,--parear, enviar/parear; quem segue esse passo a passo de facto coloca o bot online; - ONLINE → dicas de uso.
Depois de pareado (docs/ONBOARDING-TELEGRAM.md), os comandos de controlo do bot:
| Comando | O que faz |
|---|---|
/ligar | Sobe o túnel e, quando fica READY, envia automaticamente o link com a chave ?key= |
/desligar | Derruba o túnel (e revoga a chave) |
/acessar | (Re)envia o link com a chave |
/rotacionar | Gera chave nova, invalida as sessões e encerra as conexões ativas (WebSocket/streams) — revoga o acesso antigo na hora |
/status · /emergencia | estado, kill switch |
/agente · /agentes · /parar-agente | dispara, lista e cancela agentes do harness — ver abaixo |
/novo-chat · /novo-chat-wt · /worktree · /status-tarefa | tarefas: chats e worktrees do harness — ver abaixo |
O link enviado no /ligar é
https:///?key=: a chave no link autentica o túnel, é
reutilizável até rotacionar (ou derrubar o túnel) e, ao ser válida, é trocada
por uma sessão e o navegador é redirecionado para a URL limpa (sem ?key=).
Nunca se pede uma senha a digitar — o acesso é por sessão ou por chave no
link, e o acesso local abre direto.
Porque a chave pode aparecer no chat (e é esperado): o
?key=é o mecanismo de autenticação e o bot é o canal de entrega. É a chave do link, não uma "senha permanente" — não há senha a digitar para acesso.
Cada promessa destas aponta para a linha de código que a cumpre: ver docs/ARCHITECTURE.md.
Agentes: disparar skills do harness pelo bot
O dispatcher de agentes (docs/AGENTS.md — manual completo) liga o bot ao
ctx.subagents do harness: o dono escolhe uma skill da allowlist e escreve um
prompt, o host spawna um subagente in-process (sessão fresca, zero contexto
do pai) que trabalha na própria máquina, e o resultado chega ao chat.
| Comando | O que faz |
|---|---|
/agente | Abre a confirmação em 2 etapas e dispara o agente (o prompt mostrado é o que vai) |
/agentes | Lista os runs (agentes, chats e worktrees) com métrica resumida: • — — · · wt: · 📊 / (+ resumo do modelo quando terminou) |
/parar-agente | Cancela um run (os ids aparecem em /agentes) |
Porquê tanta cerimónia: o dispatch executa código na tua máquina. É a ação
que mais AUMENTA a exposição do bot — por isso exige confirmação em duas etapas
(nonce do host, como /ligar). E o agente disparado corre com as permissões do
harness e nunca recebe o token do bot nem credencial nenhuma deste plugin
(S3): o request carrega só a skill renderizada no bloco `` canónico
- o texto do teu prompt. Os runs são efémeros por desenho: vivem só em
memória — um reinício do DSH cancela tudo (
disposeem LIFO) e a lista recomeça vazia. Sem allowlist declarada (config.agents.skills), nenhum agente é disparável — fail-closed por construção.
Tarefas: chats e worktrees do harness pelo bot
Os comandos de tarefa criam coisas reais no harness: /novo-chat e
/novo-chat-wt abrem sessões do DSH com o prompt submetido (o chat corre de
verdade), e /worktree cria uma worktree git isolada — branch
guard/ em -worktrees/, irmã do checkout — sem nunca
destruir nada que exista. /status-tarefa mostra o detalhe de um run com as
métricas reais do harness (tokens, tempos, turnos); o que o host não mediu
aparece como —, nunca como número inventado.
| Comando | O que faz |
|---|---|
| `/novo-chat | |
| ` | Abre um chat novo (sessão real do DSH) com o prompt submetido — confirmação em 2 etapas |
| `/novo-chat-wt | |
| ` | O mesmo, com o chat a nascer dentro de um worktree existente (cria-o primeiro com /worktree) |
/worktree [base] | Cria a worktree git guard/ em -worktrees/ a partir de base (default HEAD) — se já existir, nada é destruído |
/status-tarefa | Detalhe de UM run: linha completa + os 12 campos de métrica reais — tokens (entrada, saída, cache lido, cache escrito, decodificados), tempos (modelo, ferramentas, primeiro token, decodificação) e contagens (turnos, passos, passos com 1º token) — com — nos campos que o host não mediu |
Exemplos reais:
/worktree fix-guard-regex
/novo-chat-wt fix-guard-regex corrige o teste que falha em test/security/host-header.test.ts
/novo-chat resume o estado do repositório e diz o próximo passo
/agentes
/status-tarefa 01J8ZC2A
Os ids de 8 caracteres aparecem em /agentes (que lista agentes, chats e
worktrees com a métrica resumida de cada um). Como /agente, o /novo-chat e o
/novo-chat-wt executam código e o /worktree altera a máquina — todos
pedem confirmação em 2 etapas: os chats mostram o prompt exato que vai
correr, e o /worktree mostra o nome (e a base, quando indicada). Textos
exatos e arquitetura:
docs/AGENTS.md e docs/ux/01-CONTRATO-BOT.md.
Provedores de mensageria
O worker do bot é neutro ao provedor: o núcleo (roteador, allowlist, pareamento, outbox)
vive em worker/surface/** e cada canal vive isolado no seu adaptador:
worker/providers/telegram/** (única carga de grammY). O boot lê o provedor ativo por DSH_GUARD_PROVIDER
(config.worker.provider, default telegram); o token é TELEGRAM_BOT_TOKEN no Telegram —
o secrets.env guarda a linha do provedor ativo e dsh-guard-setup
sabe qual é a do provedor ativo.
Adicionar um provedor novo (WhatsApp, Matrix…) é implementar o contrato neutro
ProviderAdapter e registá-lo no registry. O manual completo — arquitetura, contrato tipo-a-tipo,
a tabela de provedores com os limites de cada um e o checklist passo-a-passo para um provedor
novo — está em docs/PROVIDERS.md, e as habilidades de apoio (skills) em
.agents/skills/dsh-provider-bot e .agents/skills/dsh-telegram-provider.
Como flui um pedido (arquitetura em 8 linhas)
Telemóvel (navegador)
│ abre o link que o bot enviou: https:////?key= (a chave viaja em query)
▼
❨ Cloudflare edge ❩ TLS → HTTP/2 → WebSocket ; o TLS termina AQUI (texto claro passa pela borda)
▼
cloudflared — quick tunnel (conexão de saída apenas, sem conta, sem SLA)
▼ http://127.0.0.1:3080 (o túnel termina n