12 KiB
Impl: OPS117 — Guard de escrita dos agentes de design além dos file tools (bash)
Status: entregue (2026-09-16) Atualizado em: 2026-09-16 Issue: #1063 Intenção: docs/plans/ops117-guard-bash-agentes-design.md Appetite restante: herdado (~0,5 dia eng; um outcome verificável)
Leitura da intenção
- Outcome: a escrita em disco via bash pelos agentes
designeredesigner-degradedfora dedocs/plans/<slug>-ui-design*falha fechado (negada); gravar o próprio artefato (.html+-ui-design-assets/*.svg) segue permitido; leitura (cat/sed -n) não é bloqueada; nada de sandbox de SO nem agente twin; nada emsrc/. - O que NÃO negociar: output só no artefato; leitura livre; sem sandbox de SO; sem twin de agente; sem tocar
src//schema; o gate de file tools do OPS113 permanece. - O que reavaliar: a hipótese da intenção ("
permission.bashcasa comando, não caminho — a regra por caminho pode não existir") já foi resolvida pela evidência: com redirecionamento o glob casa oredirected_statementinteiro, incluindo o destino do redirect, logo uma regra de artefato por caminho é viável (elast-match-winspermite o allow depois do deny). O risco "semântica por caminho não existir" está morto.
Abordagem recomendada
flowchart LR
INT["Intenção OPS117: bash fora do artefato"] --> A["F1 · permission.bash declarativa\ndefault ask + deny-list\n+ allow do artefato (último)"]
A --> AG["F1 · designer.md +\ndesigner-degraded.md\n(blocos idênticos)"]
AG --> T["F2 · teste unit anti-drift\nfrontmatters idênticos"]
AG --> D["F2 · ui-design-html.md (1 nota)\n+ AGENT-OPS.md:24 (1 frase)"]
T --> V["F3 · smoke subprocesso\n+ opencode debug agent\ngate:fast · diff sem src/"]
D --> V
Opções consideradas: A | B | C
Recomendação: A — permission.bash declarativa por agente: default ask + deny-list curada dos vetores de escrita + allow explícito do artefato por último. É config pura em 2 frontmatters já existentes (o permission.edit fail-closed do OPS113 fica intocado), cabe no appetite e tem evidência empírica de que o match por caminho no redirect funciona (echo x > docs/plans/foo-ui-design.html → allow; echo x > src/a.txt → deny, arquivo ausente).
Rejeitadas: B — plugin .opencode/plugins/*.ts (tool.execute.before + throw): o input do hook é {tool, sessionID, callID} e não traz o agente (@opencode-ai/plugin@1.18.31), escopar aos dois agentes exigiria cache frágil sessionID→agent via chat.params; infra nova versionada, lint/prettier em .ts e complexidade >> appetite; C — default-deny com allowlist no bash: bloquearia leitura (cat, git diff, sed -n), fora de escopo explícito da intenção.
Decisões de engenharia (com rejeitadas)
-
Forma do guard.
- Opções: A) declarativa
permission.bashpor agente | B) plugin com hook | C) default-deny + allowlist. - Recomendação: A. Bloco idêntico nos dois frontmatters, sob o
permissionexistente:permission: edit: # intocado (OPS113) '*': deny 'docs/plans/*-ui-design*': allow bash: '*': ask '*>*': deny 'sed *-i*': deny 'tee *': deny 'cp *': deny 'mv *': deny 'rm *': deny 'mkdir *': deny 'touch *': deny 'truncate *': deny 'dd *': deny 'install *': deny 'ln *': deny 'chmod *': deny 'chown *': deny 'bash *': deny 'sh *': deny 'zsh *': deny 'python*': deny 'node *': deny 'bun *': deny 'deno *': deny 'perl *': deny 'ruby *': deny 'find *-exec*': deny 'find *-delete*': deny 'xargs *': deny 'git checkout*': deny 'git restore*': deny 'git apply*': deny '* > docs/plans/*-ui-design*': allow 'prettier --write docs/plans/*-ui-design*': allow - Curadoria (29 deny + 1 catch-all + 2 allow) em 5 classes, cada linha justificada por um vetor concreto: (1) redirecionamento
*>*cobre>e>>; (2) editores in-placesed(sed *-i*casa-ie--in-place); (3) mutadores de arquivo (cp/mv/rm/mkdir/touch/truncate/dd/install/ln/chmod/chown/tee); (4) interpretadores/shells que reabrem a escrita por dentro (bash/sh/zsh/python/node/bun/deno/perl/ruby, evidênciabash -cepython3 -c); (5) destrutivos de git (checkout/restore/apply) efind -exec/-delete/xargs. Fora da lista (permanecemask, auto-aprovados em--autoe legíveis num run interativo): leitura/inspeção (cat,sed -n,ls,rg,git diff/status/log),<(input), peers de pipeline (o parser checa cada comando). O'*': askfica explícito (baseline do ruleset e merge com o globalbash: ask; regras do agente vêm depois ⇒ agente vence). - Rejeitadas: B/C acima; e a tentação de deny-list exaustiva (infla manutenção para um risco de ferramenta de design — o resíduo é declarado, não esgotado).
- Opções: A) declarativa
-
Teste unit anti-drift (barato, incluído).
- Opções: A)
tests/unit/opencodeAgents.unit.spec.tslê os dois.mde exige o blocobashidêntico | B) sem teste (confiança no review). - Recomendação: A — o custo da divergência (um agente guardado, o outro vazando) é exatamente o anti-goal; o precedente
opencodeCommands.unit.spec.tsjá lê.opencode/*.mdsem app/DB. Extrai o sub-blocobash:do frontmatter por regex (sem dependência de YAML) e afirma: existe nos dois, é idêntico, contém'*': ask,'*>*': denye o allow* > docs/plans/*-ui-design*; o blocoeditsegue fail-closed ('*': deny+ allow do artefato). Roda notest:unitdogate:fast. - Rejeitadas: B — drift silencioso entre dois arquivos gêmeos é o modo de falha mais provável e mais grave; o teste é a única prova contínua.
- Opções: A)
-
Docs (toque mínimo, sem twinar).
- Opções: A) 1 nota em
ui-design-html.md+ 1 frase emdocs/AGENT-OPS.md:24| B) só a doutrina | C) nova seção H2 no AGENT-OPS. - Recomendação: A. A doutrina é a dona do contrato do artefato ("o bash dos agentes é guardado; escrita fora do artefato é negada") e o parágrafo OPS113 do AGENT-OPS (entrada operacional) passa a dizer que a
permissioncobre file tools + bash. Uma frase em cada; nenhuma seção nova. - Rejeitadas: B — deixa o doc operacional mentindo (diz só file tools); C — cerimônia para uma frase.
- Opções: A) 1 nota em
Componentes / mudanças
.opencode/agent/designer.md/.opencode/agent/designer-degraded.md: adicionar o blocobashda decisão 1 sob opermission(edit intocado); textos dos inegociáveis já citam "não tente contornar por shell" — sem reescrita.tests/unit/opencodeAgents.unit.spec.ts(novo): anti-drift dos dois frontmatters (decisão 2); sem app/DB (fs puro)..agents/skills/plan-issue/ui-design-html.mdedocs/AGENT-OPS.md: 1 frase cada (decisão 3).docs/changelog/2026-09-16-ops117.md(novo): linha única no formato vigente.- Migration: sem migration. Access / Consent: N/A. UI: Impeccable A — N/A sem UI de produto; nenhum arquivo em
src/é tocado.
Fases verificáveis
- F1 — Guard declarativo (~40%): bloco
bashidêntico nos dois agentes;opencode debug agent designer(subprocesso novo — config não é hot-reload) mostra o ruleset resolvido (*>* deny, artifact allow). - F2 — Teste + docs (~30%):
tests/unit/opencodeAgents.unit.spec.tsverde; 1 frase emui-design-html.mde emAGENT-OPS.md:24; changelog. - F3 — Verificação (~30%): smokes em subprocesso;
pnpm exec prettier --checknos tocados;pnpm gate:fast;git diff --statsemsrc/;pnpm push→ PRCloses #1063.
Rabbit holes / Não escopo (engenharia)
- Plugin/hook (decisão 1B), default-deny com allowlist (1C), sandbox de SO/container, bloquear leitura, agente twin, mexer no allowlist de file tools do OPS113, qualquer coisa em
src//schema/Consent. - Exaustão da deny-list (vetores exóticos ficam no resíduo declarado) e ressuscitar
design-vision/renderizador de PNG. - Não tocar consumidores OPS114/OPS116 nem outros
.opencode/agent/*.md.
Riscos e mitigação
- Resíduo honesto: não é sandbox de SO; sob
--auto, um vetor de escrita exótico fora da deny-list poderia passar. Mitigação: deny-list curada + proibição no prompt + supervisão humana; declarado no PR (não prometer mais que o entregue). - Falso-deny do artefato (allow depois do deny,
last-match-wins): o smoke provaecho x > docs/plans/<slug>-ui-design.htmlpermitido; se falhar, corrigir o padrão antes do merge (fail-closed). - Drift entre os dois frontmatters: teste anti-drift (decisão 2).
- Merge com o global
bash: ask: regras do agente vêm depois ⇒ vencem; validado peloopencode debug agent. - Prettier/eslint em
.tsnovo e em.opencode/: rodarprettier --writenos tocados;.opencode/não é ignorado (.prettierignoreignora.agents/, não.opencode/). - Disponibilidade do modelo do designer no smoke (
openai/gpt-5.6-sol): se indisponível, rodar o smoke do tier degradado (via task) ou o harness isolado equivalente, registrando o tier no PR. - Lixo de smoke no diff: limpar
docs/plans/ops117-ui-design-smoke.htmlesrc/ops117-smoke.txtantes do commit.
Aceite de engenharia
- Aceite de produto da intenção ainda coberto: escrita via bash fora de
docs/plans/*-ui-design*negada nos dois agentes; artefato gravável; leitura não bloqueada; sem sandbox/twin/src/. - Invariantes AGENTS/engineering-standards: sem tocar
src//migrations; edita o dono (frontmatter/permission existentes, doutrina existente) sem twinar; identificadores em inglês, texto de prompt/admin em pt-BR. - Testes de domínio previstos:
tests/unit/opencodeAgents.unit.spec.ts(anti-drift dos frontmatters) + smoke de permissão em subprocesso como evidência no PR; sem int (nenhum access/write path de app muda).
Simplify (2026-09-16)
'sed -i*'era subsumida por'sed *-i*'(*→.*) — removida nos dois agentes; smoke re-provou o deny desed -icom a regra única.- Teste anti-drift passou a pinar o ruleset COMPLETO (não só comparar os dois arquivos): erosão simultânea de uma regra nos dois agentes também falha o build.
- Copy de
docs/AGENT-OPS.mde da doutrina precisada para "vetores de escrita enumerados" (o guard não é sandbox de SO; o baseline éask).
Triagem pós-simplify (2026-09-16)
- Já resolvido no simplify (não reabrir): linha em branco dupla no plano (prettier), teste anti-drift pinando o ruleset completo (erosão simultânea),
'sed -i*'redundante removida, copy "vetores de escrita enumerados / não é sandbox",bashem backticks no AGENT-OPS. - Absorvido: status do plano
em execução→entregue, coerente com os aceites[x]. - Descartados:
designer-campanha-solla.mdsempermission— é persona designer e implementador (constrói a página de campanha; precisa escrever, inclusive emsrc/), fora da família OPS113/117; um guard fail-closed o quebraria (não é débito). Resíduo do--autosob vetor exótico fora da deny-list — limitação aceita e já declarada, não é sandbox de SO. - Défer com gatilho: nenhum.
Verificação executada (2026-09-16)
opencode debug agent designer/designer-degraded: 33 regrasbashidênticas (32 do agente + oaskglobal),editfail-closed intacto.- Smoke
opencode run --auto --agent designer:echo x > src/ops117-smoke.txt→pattern=*>* action=deny(arquivo ausente);echo ok > docs/plans/ops117-ui-design-smoke.html→ allow do artefato (arquivo criado);sed -i(sed *-i*)/tee/printf >/bash -c/python3 -cnegados;cat/ls(leitura) não bloqueados.designer-degradedvia task: mesmo deny. Artefatos de smoke removidos. pnpm gate:fastverde (lint + typecheck + 3383 unit).
Self-score decision-quality: 5/5 — (1) as decisões caras têm opções e rejeitadas (forma A/B/C, curadoria da deny-list, teste, docs); (2) cabe no appetite: 2 frontmatters + 1 spec + 2 frases de doc + changelog, sem código de app; (3) rabbit holes nomeados (plugin, allowlist default-deny, sandbox, exaustão da lista); (4) depth check: reusa o permission/precedente de frontmatter do OPS113 e o padrão de spec do opencodeCommands, sem criar módulo/helper/twin; (5) o outcome da intenção fica intacto — a engenharia só escolheu a forma declarativa e declarou o resíduo honestamente.