Curso / Lição 26
Lição 26 · Avançado · postura de segurança

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.

Leia primeiro (fonte primária)
docs/adr/0011-security-and-provenance.md — "as quatro restrições permanentes"

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.

Objetivos desta lição
  • Enunciar o princípio único — "o caso desconhecido nega" — e reconhecê-lo nas quatro restrições da ADR-0011.
  • Ler validateSupportPath como o validador fail-closed de manual: todo ramo nega, exceto um ok final 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.
0
restrições permanentes (ADR-0011)
T0
DEFAULT_TIER · não classificado estaciona
0
canais privados forçam redação
0
linhas literais copiadas do Hermes
O único princípio. "O caso desconhecido nega, nunca permite" (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.

O PRINCÍPIO ÚNICO · o desconhecido bate na porta — fail-open deixa passar, fail-closed barra
FAIL-OPEN (não é o Alembic) pedido desconhecido PASSA por padrão você precisa lembrar de bloquear cada ataque FAIL-CLOSED (o Alembic) pedido desconhecido NEGA err tipado só o explicitamente reconhecido entra "o caso desconhecido nega, nunca permite" ADR-0011 §1 · também por isso DEFAULT_TIER = T4

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.

Preveja antes de continuar
O 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.
T4 — o tier estacionado. 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
1Fail-closed em tudo relevante à segurança — guardas realpath, webhooks HMAC, comparações de tempo constante, Zod em toda fronteiraSegurança de caminhos do SkillStore; DEFAULT_TIER = T4; safeParse de todo subsistema
2Redação de PII antes de egressar — antes da chamada de modelo, não meramente antes de emitirpackages/etl/src/pii.ts: redactSignal + a guarda assertRedactedForEmit (mapa §3)
3Isolamento CL4R1T4S — o corpus de prompts de vendor vazado é dado para analisar, nunca um comando a seguirExcluído da ingestão-como-instrução; tratado como dado inerte
4Clean-room do tac — padrões reimplementados do zero, zero código/prompts literais, source nunca publicadoA fusão inteira é TS do zero, não source copiado
O MAPA DAS QUATRO RESTRIÇÕES · cada uma é o princípio único aplicado a uma superfície de ataque
princípio único o desconhecido nega 1 · fail-closed guardas + Zod + T4 2 · PII antes de egressar 3 · CL4R1T4S dado, não comando 4 · clean-room tac · do zero

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.

FRONTEIRA ZOD · todo subsistema valida a entrada com safeParse — sucesso vira valor tipado, falha vira err
dado externo não confiável · qualquer forma schema .safeParse() success ✓ valor tipado entra no subsistema !success ✗ err tipado · dado negado "Zod em toda fronteira" — ADR-0011 §1

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.

PIPELINE · as cinco peneiras em série de validateSupportPath — só quem passa nas cinco vira ok
cada peneira só sabe negar (err); passar é seguir adiante 1 · não vazio senão err 2 · sem \\ forward slash 3 · relativo não inicia / 4 · ≥ 2 segs + sem .. nem . 5 · 1º seg ∈ SKILL_SUPPORT _DIRS ok normalizado ↓ err↓ err↓ err↓ err↓ err qualquer falha = err tipado imediato · nunca lança · fail-closed

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 ...

FLUXOGRAMA · validateSupportPath — todo caminho leva a err, menos o que sobrevive a tudo
relPath chega vazio, \\ ou começa com / ? SIM err tipado nega · não lança NÃO ↓ algum segmento é .. ou . ? SIM err tipado traversal barrado NÃO ↓ 1º segmento ∈ SKILL_SUPPORT_DIRS? NÃO err tipado fora do allow-list SIM ↓ ok(normalized) ✓ caminho vetado e confinado — só agora ataque "references/../secret": passa no losango 1 (relativo, sem \\)… …mas morre no losango 2 (tem "..").

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:

Mentalidade fail-open (frágil): "trabalho novo começa autônomo (T1); eu adiciono um gate quando lembrar que algo é arriscado." Um caso esquecido = um agente rodando sozinho sem supervisão.
Mentalidade fail-closed (o Alembic): "trabalho novo estaciona (T4); só sobe de tier quem foi explicitamente classificado." Um caso esquecido = um trabalho parado, esperando um humano. Falha segura.

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.

FLUXO · um sinal de canal privado é redado ANTES da chamada de modelo (só texto redado cruza a fronteira)
MÁQUINA LOCAL (fronteira de confiança) sinal privado ana@x.com · +55… redactSignal() mascara email/phone… assertRedacted ForEmit · ok? privado + não redado → err [REDACTED_EMAIL] emit-safe ok(redacted) só texto redado ↑ FORA (endpoint / rede) chamada de modelo nunca vê PII bruto

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:

OS QUATRO TIPOS DE PII · MASKS de packages/etl/src/pii.ts (PiiKind)
detectado (PiiKind) substituído (MASKS) email [REDACTED_EMAIL] phone [REDACTED_PHONE] handle [REDACTED_HANDLE] token [REDACTED_TOKEN]
Por que a ordem dos padrões importa. Em 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.
PATTERNS, EM ORDEM · email → token → phone → handle (cada regex é global; a mesma string sempre reda igual)
1 · email …@….tld 2 · token sk_/ghp_… 3 · phone +55 11… 4 · handle @usuario texto redado determinístico email 1º: um endereço não vira "token" phone antes de handle: "+55…" não vira "@handle"

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.

DECISÃO · assertRedactedForEmit — só "privado E sem prova" cai no err; os outros dois ramos passam
sinal quer emitir 'redacted' in signal && === true ? SIM ok(signal) prova de redação NÃO ↓ isPrivateChannel (channel) ? NÃO (público) ok(signal) público não exige SIM (privado, sem prova) ↓ err · kind:'private_unredacted' emit bloqueado · nunca lança (ADR-0009)
Em linguagem simples: imagine um segurança na porta de saída. Ele só deixa sair quem mostra um carimbo de "já foi revisado". Quem vem de uma sala privada (WhatsApp, Discord…) e não tem carimbo é barrado na hora — não com um escândalo (exceção), mas com um "não pode, volte e revise" educado e registrado (um err). Quem vem de uma área pública não precisa de carimbo.
Na precisão do código: 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 existe como saída de redactSignal. Não há outro caminho para fabricar a prova; o compilador é o segurança.

PROVA INFORJÁVEL · só redactSignal produz redacted:true — a guarda confia no tipo, não no chamador
BusinessSignal cru · sem campo redacted redactSignal() o ÚNICO produtor RedactedSignal readonly redacted: true assertRedactedForEmit → ok tentar emitir cru como privado → err (sem o tipo, sem passe) não há atalho: o tipo só nasce aqui

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.

Exemplo resolvido · um sinal vindo do Discord, ainda não redado, quer ir para a chamada de modelo
1
Identifique o canal. O sinal veio do discord. isPrivateChannel('discord')true (está em PRIVATE_CHANNELS).
2
Tem prova de redação? O sinal é um BusinessSignal cru — não tem o campo redacted: true. Logo alreadyRedacted = false.
3
Aplique a guarda. Privado + sem prova → assertRedactedForEmit retorna err({ kind: 'private_unredacted', … }). O emit é bloqueado — fail-closed.
4
Corrija o fluxo. Passe o sinal por redactSignal() primeiro: o email/telefone viram [REDACTED_…] e o resultado é um RedactedSignal (redacted: true).
5
Reaplique a guarda. Agora alreadyRedacted = true → ok(signal). Só texto redado cruza a fronteira para o endpoint do modelo.
Agora você: um sinal vindo de um blog público (channel = 'web'), sem redação. A guarda bloqueia ou deixa passar? Decida antes de revelar.
Deixa passar — 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.

CL4R1T4S · a mesma entrada, dois destinos — "como instrução" sequestra; "como dado" é inerte
corpus vazado + README de injeção como instrução ✗ agente executa o payload sequestrado (prompt-injection) como dado ✓ apenas analisado (texto inerte) seguro · 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.

✗ copiar código/prompt literal ✗ publicar o source ✓ reimplementar o padrão do zero ✓ citar o próprio Alembic

10 · Proveniência amarra tudo

Proveniência: fail-closed visto de outro ângulo

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.

PROVENIÊNCIA · todo fato carrega {fonte, data, hash} → o sistema sabe se confia
fato ingerido carimba proveniência { fonte · data · hash } origem vetada? SIM confiável NÃO suspeito (isola)

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.

O MODELO DE AMEAÇA · a fronteira de confiança e onde cada uma das 4 restrições guarda o cruzamento
MÁQUINA LOCAL · fronteira de confiança fail-closed em tudo (§1) · proveniência {fonte,data,hash} funil · stores · SkillStore guarda de path (§1) · Zod (§1) DEFAULT_TIER = T4 não classificado estaciona (§1) §2 redige antes ↑ endpoint do modelo FORA · nunca vê PII dado de terceiros raspado · vazado CL4R1T4S · tac §3 como dado §4 do zero ↑ toda seta que cruza a borda é guardada e fail-closed — sair (§2) e entrar (§3,§4)

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ãoFail-open (não é o Alembic)Fail-closed (o Alembic · ADR-0011)
Caso desconhecidopassa por padrãonega por padrão
Tier de trabalho não classificadoautônomo (otimista)DEFAULT_TIER = T4 · estaciona
Caminho inesperado no SkillStoreaceito se "parecer ok"err a menos que ∈ allow-list
Sinal privado sem redaçãopode egressarassertRedactedForEmiterr
Falha não tratadapasse silencioso ou crashnegação tipada, nunca lança (ADR-0009)
Custo de esquecer um casovazamento / execução indevidatrabalho parado, seguro
fail-open: superfície de ataque cresce com o que você esquecefail-closed: superfície de ataque é só o que você abriu de propósito
SUPERFÍCIE DE ATAQUE · fail-open cresce a cada caso esquecido; fail-closed só o explicitamente aberto
→ casos que você esqueceu de tratar (tempo) superfície de ataque fail-open ↗ fail-closed → (plano) Cada caso não previsto abre uma brecha no modo fail-open; no fail-closed ele já está negado, então a linha não sobe.

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.

LINHA DO TEMPO · redigir no egresso (✓) cobre a chamada de modelo; redigir só no emit (✗) já vazou antes
✓ ALEMBIC · redige no egresso sinal privado REDIGE aqui chamada modelo emit só texto redado cruza a rede ✗ INGÊNUO · redige só antes de emitir sinal privado chamada modelo REDIGE (tarde) PII bruto já viajou pela rede ✗

Confusões comuns

"Fail-closed significa que o sistema quebra em entrada ruim." O oposto — ele retorna um erro tipado, fail-closed (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.
"Redar antes de emitir já basta." Não: emitir é o fim do caminho; egressar é qualquer ponto em que o dado sai da máquina — e a chamada de modelo é um desses pontos. Redar só antes de emitir ainda mandaria PII bruto pela rede ao endpoint. Por isso assertRedactedForEmit mora no funil, antes da chamada, não na saída final.
"A regra de clean-room é só cautela legal." É também por que este curso é confiável: toda lição cita o source do próprio Alembic porque uma cópia literal do Hermes seria uma violação. Reimplementar do zero é tanto a postura legal quanto a razão de o código ser genuinamente entendido, não colado.

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.

A CASCA DE SEGURANÇA · web/mídia entram → PII-redação + path-guard + fail-closed envolvem o funil → só dado vetado emite
dado externo (web/mídia) raspado · transcrito · vazado §3/§4 SkillStore · entrada guarda de path-traversal (§1) A CASCA DE SEGURANÇA · esta lição fail-closed em tudo (§1) · proveniência {fonte,data,hash} o funil de destilação (Lição 15) redactSignal antes do modelo · assertRedactedForEmit antes da escrita DEFAULT_TIER = T4 não classificado estaciona (§1) §2 redige antes ↑ endpoint do modelo FORA · nunca vê PII bruto só dado vetado e PII-safe emite verifiedSignals redados → learnings (loop) · stores append-only Setas laranja = entrada de dado não confiável (filtrada por §3/§4 e pela guarda de path). Setas verdes = saída só com prova/redação. Toda aresta que cruza a borda é fail-closed.
Clique para acender peça por peça — da entrada não confiável à emissão PII-safe.
Onde você está na metodologia: esta lição é a camada de postura — não uma etapa do pipeline, mas o conjunto de guardas que envolve toda etapa onde dado cruza a fronteira de confiança. A ingestão traz dado de terceiros (a casca §3/§4 o trata como inerte); o funil destila (a casca §2 reda antes do egresso, §1 estaciona o não classificado); o consumo lê só o que já está vetado e PII-safe. Veja como essa casca se sobrepõe ao resto no mapa interativo da metodologia.

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:

Lição 15 · O funil
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.
Lição 11 · webSearch / webExtract
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.
Lição 13 · transcribe / analyzeImage
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.
Lição 24 · A trilha de ADRs
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.
Lição 16 · As quatro invariantes
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
O que ler na saída: o teste que importa é 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/etlpii.test.ts; @alembic/hermesskill-store.test.ts).
Experimente · veja uma string de PII fingida ser redada ANTES de egressar (~2 min)
1
Entre no repo e garanta o build. cd /Users/acf/Documents/Projects/appfy/alembic e então pnpm -r build — os pacotes precisam estar compilados para os tipos resolverem.
2
Rode a suíte do ETL. pnpm --filter @alembic/etl test. Ela exercita redactText (mascara email/phone/handle/token) e o portão assertRedactedForEmit.
3
Veja a redação acontecer. O teste 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.
4
Veja o fail-closed. O teste 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.
5
Confronte com a guarda de path. 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.
Agora você: e se você passar redactText('um sinal de negócio perfeitamente limpo') — sem nenhum email/telefone/token? O campo redacted volta true ou false? Decida antes de revelar.
Volta 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.
Comandos vizinhos: a casca de segurança roda dentro do funil, então 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.

Princípio
Qual é o único princípio da ADR-0011?
clique pra virar ↻
Resposta
"O caso desconhecido nega, nunca permite" (§1). Fail-closed por padrão — é também por isso que DEFAULT_TIER = T4.
Path-traversal
Que padrão é validateSupportPath?
clique pra virar ↻
Resposta
Allow-listing fail-closed: todo ramo nega (vazio/\\/absoluto/../fora de SKILL_SUPPORT_DIRS) exceto um ok final. Pura, nunca lança.
PII
Por que redar "antes de egressar", não antes de emitir?
clique pra virar ↻
Resposta
O endpoint do modelo está fora da fronteira de confiança. Redar só antes de emitir ainda mandaria PII bruto pela rede. A guarda é assertRedactedForEmit.
CL4R1T4S
Por que o corpus vazado nunca é "ingerido como instrução"?
clique pra virar ↻
Resposta
Tem um README de payload de injeção. Tratá-lo como dado (a analisar) e não comando (a seguir) é defesa contra prompt-injection na ingestão.

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.

1. A 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?
Correto: b. Redar só antes de emitir ainda enviaria PII bruto pela rede ao modelo; a ADR empurra a redação para o ponto de egresso (onde o dado sairia da máquina), e a chamada de modelo é um desses pontos. a confunde segurança com performance — a regra existe pela fronteira de confiança, não pela velocidade. c inverte a motivação: o objetivo é privacidade, não economia de tokens (a redação até pode mudar o tamanho, mas não é o ponto). d é factualmente falso — o modelo processaria a PII sem problema; o risco é justamente esse, por isso ela nunca pode chegar lá.
2. validateSupportPath rejeita .., caminhos absolutos e backslashes, permitindo só caminhos sob um subdir aprovado. Qual padrão de design é esse?
Correto: d. A função enumera o que é permitido (relativo, sem traversal, primeiro segmento em 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.
3. Por que o corpus CL4R1T4S é "nunca ingerido como instrução"?
Correto: c. A 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.
4. Num sistema fail-closed, o que acontece com um trabalho que ninguém classificou de tier?
Correto: a. 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.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Como o tipo 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.