Proveniência & segurança: fail-closed por padrão
Um sistema que ingere logs de chat privado, raspa a web e grava arquivos que um agente nomeia precisa ser paranoico por construção — não por lembrar de checar. A ADR-0011 define quatro restrições permanentes — fail-closed em tudo, redação de PII antes de um byte sair da máquina, isolamento de um corpus de prompts vazado e uma regra de clean-room — e elas não são slogans: aparecem como guardas reais no código que você já conheceu. Esta lição conecta a política à implementação: a fronteira Zod, a defesa realpath/path-traversal no SkillStore, o fail-closed DEFAULT_TIER = T4, e a redação de PII antes da chamada de modelo.
Esta lição destila a ADR-0011 + o código que a torna real: packages/etl/src/pii.ts (redação antes de egressar) e packages/hermes/src/skills/skill-store.ts (a guarda de path-traversal). Todo número e linha de código tem fonte (rodapé). Por que importa pra missão: a fusão ingere dado privado e segue dado de terceiros — sem esta postura, um chat vazado ou um README malicioso viraria execução de código.
- Enunciar o princípio único — "o caso desconhecido nega" — e reconhecê-lo nas quatro restrições da
ADR-0011. - Ler
validateSupportPathcomo o validador fail-closed de manual: todo ramo nega, exceto umokfinal vetado. - Explicar por que PII é redada antes de egressar (antes da chamada de modelo), não antes de emitir — e ler a guarda
assertRedactedForEmit. - Distinguir fail-open de fail-closed e por que
DEFAULT_TIER = T4é a forma mais profunda da postura.
ADR-0011 §1). Fail-closed é a postura padrão para tudo relevante à segurança — e é a mesma ideia que DEFAULT_TIER = T4 (trabalho não classificado estaciona) e o contrato de nunca-lança (uma falha não tratada vira uma negação tipada, não um passe silencioso).01 · O único princípio: a porta trancada por padrão
Antes das quatro restrições, entenda a ideia que as une — porque todas são a mesma ideia vista de ângulos diferentes. Pense numa porta: ela pode ter dois padrões. Ou está destrancada e você tranca o que sabe ser perigoso (fail-open), ou está trancada e só abre para quem você reconheceu explicitamente (fail-closed). O Alembic escolhe a segunda em tudo relevante à segurança.
Por que isso é tão poderoso? Porque segurança por omissão é frágil — basta esquecer de bloquear um caso e o sistema vaza. Segurança por negação padrão é robusta: o caso que você nunca imaginou já está barrado, porque tudo que não foi explicitamente permitido é negado. Guarde essa frase; ela reaparece em cada uma das quatro restrições e em cada SVG abaixo.
DEFAULT_TIER do motor é o tier que um trabalho recebe quando ninguém o classificou explicitamente. Dos quatro tiers (T1 autônomo barato → T4 estacionado/humano), qual você espera que seja o padrão num sistema fail-closed? Comprometa-se com um palpite antes de revelar.
DEFAULT_TIER: Tier = Tier.T4 (packages/contracts/src/tier.ts:51). Se você chutou "T1, pra ser produtivo", caiu na intuição fail-open: deixar o desconhecido rodar sozinho. O motor faz o oposto — trabalho não classificado estaciona e espera um humano (isParked(tier), :63). Autonomia não classificada é impossível por construção. É o princípio único como default, não como guarda que você chama.02 · As quatro restrições permanentes
A ADR-0011 não é uma lista de boas intenções: cada restrição aponta para um lugar concreto no código. Veja as quatro de uma vez, depois desça em cada uma.
| # | Restrição (ADR-0011) | Onde vive no código |
|---|---|---|
| 1 | Fail-closed em tudo relevante à segurança — guardas realpath, webhooks HMAC, comparações de tempo constante, Zod em toda fronteira | Segurança de caminhos do SkillStore; DEFAULT_TIER = T4; safeParse de todo subsistema |
| 2 | Redação de PII antes de egressar — antes da chamada de modelo, não meramente antes de emitir | packages/etl/src/pii.ts: redactSignal + a guarda assertRedactedForEmit (mapa §3) |
| 3 | Isolamento CL4R1T4S — o corpus de prompts de vendor vazado é dado para analisar, nunca um comando a seguir | Excluído da ingestão-como-instrução; tratado como dado inerte |
| 4 | Clean-room do tac — padrões reimplementados do zero, zero código/prompts literais, source nunca publicado | A fusão inteira é TS do zero, não source copiado |
A restrição 1 lista quatro mecanismos concretos — guardas realpath, webhooks HMAC, comparações de tempo constante e Zod em toda fronteira. O último é o mais onipresente: todo subsistema do motor faz safeParse da entrada antes de tocá-la, e o resultado é binário — ou um valor tipado válido, ou um err. Dado externo nunca entra no sistema sem passar por essa porta.
03 · Restrição 1 no código: a guarda de path-traversal
Você viu o SkillStore na Lição 12. Sua espinha de segurança é validateSupportPath — uma função pura que recusa qualquer caminho relativo que pudesse escapar do diretório da skill. Ela espelha o has_traversal_component + _resolve_skill_target do Hermes (confinado a ALLOWED_SUBDIRS) e é o validador fail-closed de manual: lista o que permite (via SKILL_SUPPORT_DIRS / isSupportDir) e nega todo o resto.
// packages/hermes/src/skills/skill-store.ts:404-433 (condensado) const validateSupportPath = (relPath: string): Result<string, Error> => { if (relPath.length === 0) return err(new Error('file path is required.')); if (relPath.includes('\\')) return err(…'use forward slashes.'); // sem truques de backslash if (relPath.startsWith('/')) return err(…'must be relative.'); // sem caminhos absolutos const segments = relPath.split('/').filter((s) => s.length > 0); for (const segment of segments) { if (segment === '..' || segment === '.') // sem segmentos de traversal return err(…'path traversal is not allowed.'); } const first = segments[0]; if (first === undefined || !isSupportDir(first)) // deve ser um subdir PERMITIDO return err(…'first segment must be one of …'); return ok(normalized); // só agora: um caminho relativo vetado e confinado };
Note a forma: todo ramo é uma negação exceto o ok final. Um ../../etc/passwd controlado por atacante é rejeitado na checagem de ..; um sorrateiro references/../../secret também é rejeitado. A função é pura e nunca lança, então compõe limpa no mundo Result — falhas de segurança surgem como erros tipados, fail-closed (ADR-0011 §1, "não passes silenciosos"). Há ainda uma quinta guarda no source: segments.length < 2 rejeita um nome solto sem subdir (deve ser references/api.md, não só api.md).
Outra forma de ver a mesma função é como uma esteira de peneiras em série: o caminho só chega ao fim se passar, em ordem, por cinco peneiras — e cada uma só sabe negar. Não há atalho nem caminho lateral.
04 · Anatomia de uma função fail-closed (fluxograma)
A melhor maneira de "ver" o fail-closed é seguir um caminho hostil pela função. Cada losango é uma checagem; cada "SIM" para uma checagem de perigo leva direto à negação. Só sobreviver a todos os losangos chega no ok. Siga o ataque references/../secret pelas setas — ele morre no losango do ...
Repare no que o fluxograma não tem: nenhuma seta que diga "se eu não reconheci o problema, deixa passar". Não há saída fail-open. A única forma de chegar ao ok é sobreviver a todas as negações — o caminho feliz é o caminho mais estreito, e é de propósito.
05 · Restrição 1, de novo: fail-closed por tier padrão
A expressão mais profunda de fail-closed não é uma guarda que você chama — é o padrão. DEFAULT_TIER = T4 significa que qualquer trabalho não explicitamente classificado como autônomo é estacionado, esperando um humano (Lição 24; a própria ADR-0011 aponta isso para a ADR-0004). A linha real é minúscula e definitiva:
// packages/contracts/src/tier.ts:51,63 export const DEFAULT_TIER: Tier = Tier.T4; // o desconhecido estaciona export const isParked = (tier: Tier): boolean => // e "estacionado" é checável tier === DEFAULT_TIER;
A ADR-0011 traça a linha ela mesma: "o caso desconhecido nega, nunca permite (é também por isso que DEFAULT_TIER = T4)". Autonomia não classificada é impossível por construção, não por lembrar de checar. Compare as duas mentalidades:
06 · Restrição 2: PII antes de egressar, não antes de emitir
A palavra sutil é egressar. Seria fácil redar PII só antes de mostrar um resultado a um usuário. A ADR-0011 exige mais: um Signal derivado de um canal privado (WhatsApp, Discord, Skool, Circle) é "redado de PII antes de sair da máquina local — antes da chamada de modelo, não meramente antes de emitir". O modelo de ameaça assume que o próprio endpoint do modelo está fora da fronteira de confiança, então dado privado bruto nunca pode estar num payload de requisição.
Onde está, no código, esse "antes de egressar"? Em packages/etl/src/pii.ts. O módulo é puro e determinístico — a mesma entrada sempre produz a mesma saída redada, então é trivialmente testável e seguro de chamar em qualquer ponto do funil. Ele detecta e mascara quatro tipos de PII, e a tabela de máscaras é literal:
pii.ts os PATTERNS são aplicados em ordem: email antes de token (para um endereço não ser confundido com credencial); phone antes de handle (para um número com + à frente não ser mal-detectado). Detalhe pequeno, mas é determinismo de verdade: a sequência fixa garante que a mesma string sempre redê do mesmo jeito.07 · A guarda de egresso, no código real
A redação por si só não basta — algo precisa impedir que um sinal privado não-redado escape. Essa é a função assertRedactedForEmit: um portão fail-closed que só deixa passar um sinal de canal privado se ele já provou ter passado pela redação. Repare que ela nunca lança — retorna um err tipado (EmitBlocked), no estilo Result do motor inteiro.
// packages/etl/src/pii.ts:178-192 (condensado) export const assertRedactedForEmit = ( signal: BusinessSignal | RedactedSignal, channel: string, ): Result<RedactedSignal | BusinessSignal, EmitBlocked> => { const alreadyRedacted = 'redacted' in signal && signal.redacted === true; if (alreadyRedacted) return ok(signal); // já provou redação → passa if (!isPrivateChannel(channel)) return ok(signal …); // canal público → passa return err({ // privado + não redado → NEGA kind: 'private_unredacted', channel, signalId: plain.id, message: `signal … from private channel '…' must be redacted before emit`, }); };
Leia a lógica como três portas: (1) se o sinal carrega a prova redacted: true, passa; (2) se o canal é público, passa (canais públicos não exigem redação forçada); (3) caso contrário — privado e sem prova de redação — nega com um erro tipado. O tipo RedactedSignal tem o campo redacted: true literal: você não consegue forjar a prova sem passar por redactSignal. A segurança está no tipo, não na boa vontade do chamador.
err). Quem vem de uma área pública não precisa de carimbo.RedactedSignal é um tipo com readonly redacted: true, produzido apenas por redactSignal. assertRedactedForEmit faz um type-narrowing por 'redacted' in signal; isPrivateChannel é um type guard sobre a tupla PRIVATE_CHANNELS (['whatsapp','discord','skool','circle']). O retorno é Result<…, EmitBlocked> com kind: 'private_unredacted' — o vazamento é impossível porque a prova de redação é inforjável pelo sistema de tipos, e a negação é um valor, não um throw (ADR-0009).A frase "a segurança está no tipo" merece um diagrama. redacted: true não é um booleano que você seta à mão — é um tipo literal que só existe como saída de redactSignal. Não há outro caminho para fabricar a prova; o compilador é o segurança.
08 · Decida na mão: esse sinal pode egressar? (passo a passo → agora você)
Você viu a guarda. Agora rode-a na cabeça, devagar, sobre um caso concreto — depois um caso é seu. Recuperar o procedimento (não só ler o veredito) é o que fixa.
discord. isPrivateChannel('discord') → true (está em PRIVATE_CHANNELS).BusinessSignal cru — não tem o campo redacted: true. Logo alreadyRedacted = false.assertRedactedForEmit retorna err({ kind: 'private_unredacted', … }). O emit é bloqueado — fail-closed.redactSignal() primeiro: o email/telefone viram [REDACTED_…] e o resultado é um RedactedSignal (redacted: true).alreadyRedacted = true → ok(signal). Só texto redado cruza a fronteira para o endpoint do modelo.channel = 'web'), sem redação. A guarda bloqueia ou deixa passar? Decida antes de revelar.
ok(signal). isPrivateChannel('web') é false, então a guarda retorna ok sem exigir redação. A redação forçada só vale para os quatro canais privados; conteúdo público não carrega PII de terceiros que precise ser mascarada por essa regra. Dica: o procedimento é sempre o mesmo — (1) o canal é privado? (2) há prova de redação? Só "privado E sem prova" nega.09 · Restrições 3 & 4: não siga dados, não copie source
As duas últimas são sobre disciplina, não checagens em runtime. Isolamento CL4R1T4S: um corpus vazado de prompts de vendor (e seu README de payload de injeção) "é isolado e nunca ingerido como instrução; é dado para analisar, nunca um comando a seguir". É defesa contra prompt-injection na camada de ingestão — o corpus é texto inerte, nunca executado.
Clean-room do tac: o tac é um blueprint de licença educacional, então "seus padrões são reimplementados do zero, com zero código ou prompts literais, e seu source nunca é publicado". A fusão inteira do @alembic/hermes é uma reimplementação TypeScript do zero precisamente por causa dessa regra — que é também por que as lições citam o source do próprio Alembic, nunca o Python do Hermes.
10 · Proveniência amarra tudo
A regra de orquestração do CLAUDE.md "SEMPRE cite a fonte" e os stores content-addressed (SHA-256 sobre JSON canônico, Lição 28) significam que todo fato ingerido carrega uma fonte, uma data e um hash. Proveniência não é uma feature separada — é o que permite ao sistema saber se um dado é confiável (um Learning vetado) ou suspeito (um payload CL4R1T4S). Fail-closed + proveniência são a mesma postura por dois ângulos: negue o desconhecido, e sempre saiba de onde uma coisa veio.
Tudo nesta lição assenta sobre um único desenho: onde fica a fronteira de confiança. Dentro dela está a sua máquina local; fora dela estão o endpoint do modelo, a rede e o dado de terceiros (raspado, vazado, ingerido). Cada uma das quatro restrições é uma guarda exatamente no ponto em que algo cruza essa fronteira — e cada cruzamento é fail-closed.
11 · Fail-open vs fail-closed: o trade-off lado a lado
Toda a lição gira em torno de uma escolha de postura: o que acontece com o caso desconhecido. A tabela mostra os números/comportamentos; o gráfico abaixo torna o trade-off visível — fail-open é "produtivo" até o primeiro caso esquecido virar um vazamento.
| Dimensão | Fail-open (não é o Alembic) | Fail-closed (o Alembic · ADR-0011) |
|---|---|---|
| Caso desconhecido | passa por padrão | nega por padrão |
| Tier de trabalho não classificado | autônomo (otimista) | DEFAULT_TIER = T4 · estaciona |
| Caminho inesperado no SkillStore | aceito se "parecer ok" | err a menos que ∈ allow-list |
| Sinal privado sem redação | pode egressar | assertRedactedForEmit → err |
| Falha não tratada | passe silencioso ou crash | negação tipada, nunca lança (ADR-0009) |
| Custo de esquecer um caso | vazamento / execução indevida | trabalho parado, seguro |
12 · A confusão central: "antes de emitir" vs "antes de egressar"
Se você guardar uma única distinção desta lição, que seja esta — e ela é geométrica. Numa linha do tempo do dado, "egressar" (sair da máquina) acontece antes de "emitir" (mostrar o resultado final). A chamada de modelo já é um egresso. Redar só no fim deixaria PII bruto viajar pela rede.
Confusões comuns
ADR-0011 §1, "não passes silenciosos") e nunca lança (ADR-0009). O trabalho é negado ou estacionado, de forma limpa; nada quebra e nada tem sucesso em silêncio.assertRedactedForEmit mora no funil, antes da chamada, não na saída final.13 · Como isso se encaixa
A postura desta lição não é uma feature isolada — é uma casca de guardas em volta do funil de destilação (Lição 15). Olhe a peça inteira: dado bruto entra a montante (alguma parte vinda da web e de mídia); o funil destila; e em cada ponto onde algo cruza a fronteira de confiança há um portão fail-closed. A redação de PII antes de egressar guarda a saída para o endpoint do modelo; a guarda de path-traversal guarda a entrada de skills no SkillStore; o fail-closed por tier (DEFAULT_TIER = T4) é a postura por baixo de tudo. As demais peças da metodologia ou alimentam dado que esta casca filtra, ou dependem de que ela já filtrou.
As peças que se conectam a esta
A postura de segurança toca cinco outras lições — cada uma abre uma peça que aqui aparece como uma aresta guardada:
Porque conecta: a invariante ① do funil — "PII redada antes de egressar" — é exatamente a regra que esta lição abre por dentro (
redactSignal antes do modelo + assertRedactedForEmit antes da escrita). O funil é onde a guarda de PII roda; aqui você vê por que e como ela é fail-closed.Porque conecta: a web é uma das arestas de egresso (a requisição sai da máquina) e de entrada de dado de terceiros. O conteúdo raspado é dado a analisar, nunca comando a seguir (§3) — a mesma disciplina anti-injeção do corpus CL4R1T4S, aplicada ao que vem da rede.
Porque conecta: transcrições de canais privados (WhatsApp, Discord) são uma fonte clássica de PII — é o sinal que
redactSignal precisa mascarar antes de qualquer chamada de modelo. A mídia entra crua; a casca §2 garante que só texto redado cruza a fronteira.Porque conecta: as quatro restrições desta lição são a
ADR-0011; o "nunca lança" das guardas é a ADR-0009; e o DEFAULT_TIER = T4 aponta para a ADR-0004. A Lição 24 mostra como uma decisão vira uma restrição permanente — esta lição é essa trilha tornada código.Porque conecta: "PII antes de egressar" e "fail-closed em tudo" são invariantes que o sistema nunca pode regredir — a Lição 16 generaliza o que aqui você viu fechar cada portão, e o teste de invariante é o que prova que a regressão não passa.
14 · Na prática
Chega de teoria — prove as guardas. A casca de segurança é, antes de tudo, código testado: a redação de PII e o portão de egresso vivem em packages/etl/src/pii.ts, e a suíte pii.test.ts fixa cada ramo fail-closed. A guarda de path-traversal vive em outro pacote (@alembic/hermes), então tem sua própria suíte. Rode as duas:
# 1) A redação de PII + o portão de egresso (packages/etl/src/pii.ts) $ pnpm --filter @alembic/etl test # 2) A guarda de path-traversal do SkillStore (packages/hermes/src/skills/skill-store.ts) $ pnpm --filter @alembic/hermes test # saída esperada (forma ilustrativa — as contagens dependem da build): # ✓ src/pii.test.ts (redactText masks every supported PII kind) # ✓ src/pii.test.ts (assertRedactedForEmit blocks an unredacted # private-channel signal — fail-closed err) # ✓ src/skills/skill-store.test.ts (rejects a ".." escape in the relative path) # ✓ src/skills/skill-store.test.ts (rejects a file outside the allowed support subdirs) # Test Files passed
assertRedactedForEmit blocks an unredacted private-channel signal (fail-closed err) — ele prova a invariante central desta lição: um sinal de canal privado não redado não emite. O segundo, rejects a ".." escape, prova a guarda de path com o ataque real references/../../escape.md. [uncertain] os rótulos exatos e a forma da saída do Vitest dependem da build; o que é fato verificado no source são os nomes dos testes e os pacotes (@alembic/etl → pii.test.ts; @alembic/hermes → skill-store.test.ts).cd /Users/acf/Documents/Projects/appfy/alembic e então pnpm -r build — os pacotes precisam estar compilados para os tipos resolverem.pnpm --filter @alembic/etl test. Ela exercita redactText (mascara email/phone/handle/token) e o portão assertRedactedForEmit.masks every supported PII kind passa uma string fingida — mail a@b.com call +1 (415) 555-0199 ping @bob key sk-… — e afirma que a saída contém [REDACTED_EMAIL], [REDACTED_PHONE], [REDACTED_HANDLE], [REDACTED_TOKEN] e não contém a@b.com. É a prova de que a PII some antes de qualquer egresso.blocks an unredacted private-channel signal chama assertRedactedForEmit(signal('…a@b.com'), 'whatsapp') e afirma gate.ok === false com kind === 'private_unredacted'. Privado + sem prova = err, nunca um throw.pnpm --filter @alembic/hermes test roda o ataque references/../../escape.md contra writeFile e afirma o erro traversal is not allowed — a outra metade da casca §1.redactText('um sinal de negócio perfeitamente limpo') — sem nenhum email/telefone/token? O campo redacted volta true ou false? Decida antes de revelar.
false — é um no-op. redacted só é true quando a soma das contagens (email + phone + handle + token) é maior que zero. Texto limpo sai idêntico, com redacted: false — o teste is a no-op on clean text fixa exatamente isso (packages/etl/src/pii.test.ts). Lição: a redação é determinística e honesta — não inventa máscaras onde não há PII, e sinaliza claramente quando não tocou em nada.alembic distill <corpus> --offline (Lição 15) exercita a redação em fluxo — procure t1PiiBlocked = 0 no relatório (não-zero = um sinal privado não redado tentou egressar). E o alembic doctor --client-stack valida a coerência de adapters/registry offline, sem rede — coerente com a postura de não egressar sem necessidade.Fixe os conceitos (flashcards)
Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.
DEFAULT_TIER = T4.validateSupportPath?\\/absoluto/../fora de SKILL_SUPPORT_DIRS) exceto um ok final. Pura, nunca lança.assertRedactedForEmit.Revisão cumulativa — recupere de memória
Antes de clicar: responda de cabeça. As quatro opções têm o mesmo tamanho de propósito — sem pista pela forma.
ADR-0011 exige redação de PII "antes de egressar … antes da chamada de modelo, não meramente antes de emitir". Por que a ênfase em antes da chamada de modelo?validateSupportPath rejeita .., caminhos absolutos e backslashes, permitindo só caminhos sob um subdir aprovado. Qual padrão de design é esse?SKILL_SUPPORT_DIRS) e nega todo o resto com err tipado. a é exatamente o oposto — fail-open bloquearia só padrões "conhecidos ruins" e deixaria o desconhecido passar; aqui o desconhecido é negado. b e c são padrões de concorrência/resiliência, não de validação de segurança — não têm relação com decidir se um caminho é seguro.ADR-0011 §3 o isola como dado inerte; se o sistema o executasse como instrução, o payload de injeção poderia sequestrar o agente. a erra a razão — a questão é segurança (injeção), não direitos autorais (essa é a preocupação do tac, §4). b inventa um limite técnico de tamanho que não é o motivo. d é falso: o conteúdo é legível; o perigo é justamente segui-lo. A regra — "dado para analisar, nunca um comando a seguir" — é a defesa.DEFAULT_TIER: Tier = Tier.T4 (tier.ts:51) e isParked (:63) tornam o desconhecido estacionado por padrão. b é a postura fail-open (otimista) que a ADR rejeita — rodar o desconhecido sozinho é exatamente o que se quer impedir. c viola "não passes silenciosos": nada some sem registro. d contradiz o contrato nunca-lança (ADR-0009) — a falha vira negação tipada, não crash.RedactedSignal torna a prova de redação inforjável?", "Por que webhooks HMAC e comparação de tempo constante entram na restrição 1?", "Onde, no funil, assertRedactedForEmit é chamada?". É só dizer.