mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
chore(OPS117): guard de escrita via bash nos agentes de design (Closes #1063)
This commit is contained in:
parent
04135df820
commit
06b118a4ff
7 changed files with 306 additions and 1 deletions
|
|
@ -19,6 +19,7 @@ Mesma doutrina para os dois papéis (fonte única — os prompts dos agentes apo
|
|||
- **Criticar:** o `designer` relê a **implementação renderizada** (screenshots do app) contra o artefato aprovado e devolve **lista numerada de ajustes concretos** — hierarquia (o CTA primário domina?), contraste, tipografia, espaçamento, mobile, acessibilidade. **A referência é o alvo — nunca a critique.**
|
||||
- **Visão nativa (fail-closed):** leia screenshots, prints e referências **direto com a tool Read**; nunca peça ao humano para descrever o que você pode ver. Se a leitura da imagem falhar (modelo da sessão sem visão), **pare** e peça a troca de modelo via `/models` antes de julgar a tela; **nunca descreva o que não viu**.
|
||||
- **Tier degradado:** o `designer-degraded` cria/estende/critica, mas **marca todo output `DEGRADED`** e **nunca certifica** — exige sign-off humano explícito. `DEGRADED` não é design aprovado.
|
||||
- **Escrita só no artefato (file tools + bash):** os dois agentes têm `permission` fail-closed — `edit`/`write`/`patch` negados fora de `docs/plans/<slug>-ui-design*` e o **bash** nega os vetores de escrita enumerados (redirecionamento, `sed -i`, `tee`, shells/interpretadores) fora do artefato; o resto do shell fica em `ask`. Não tente contornar por shell (o guard cobre os vetores enumerados, não é sandbox de SO) — leia/inspecione à vontade (`cat`, `sed -n`, `git diff`) e grave o artefato pelas file tools.
|
||||
|
||||
## O que o design DEVE ter (fidelidade mínima)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,39 @@ permission:
|
|||
edit:
|
||||
'*': 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
|
||||
---
|
||||
|
||||
# Persona: Designer degradado do Teqo (tier de fallback)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,39 @@ permission:
|
|||
edit:
|
||||
'*': 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
|
||||
---
|
||||
|
||||
# Persona: Designer de referência do Teqo
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ main → push dispara deploy.yml sozinho (ou workflow_dispatch manual) → prefl
|
|||
|
||||
**Skills:** `plan-issue` (intenção + Issues) → `work-issue` (humano: Issue já claimada → impl plan → confirmação → execução) ou `agent-work-issue` (pool: já claimada → impl plan → execução sem pausa) → `/simplify` → `capture-review-debts` → PR `--base main` (GitHub) → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = Issues do GitHub (`github.com/fsolla/teqo/issues`).
|
||||
|
||||
**Design hi-fi (OPS113):** item que muda UI passa pelo agente `designer` (`model: openai/gpt-5.6-sol`; crítica contestada/pixel-critical em `openai/gpt-6-astra`, mesmo agente via `/models`); a doutrina `.agents/skills/plan-issue/ui-design-html.md` é a fonte única do artefato `docs/plans/<slug>-ui-design.html` (+ `-ui-design-assets/*.svg`), que é a fonte de verdade do port — o implementador porta classe-a-classe. Indisponibilidade/quota desce a ladder — `opencode-go/deepseek-v4.1-flash` (agente `designer-degraded`) → `deepseek/deepseek-flash` (inline pelo orquestrador) — sempre gravando o tier no PR (`Design tier:`); tier degradado marca `DEGRADED`, não certifica e para em sign-off humano (triggers no work-issue: OPS115).
|
||||
**Design hi-fi (OPS113):** item que muda UI passa pelo agente `designer` (`model: openai/gpt-5.6-sol`; crítica contestada/pixel-critical em `openai/gpt-6-astra`, mesmo agente via `/models`); a doutrina `.agents/skills/plan-issue/ui-design-html.md` é a fonte única do artefato `docs/plans/<slug>-ui-design.html` (+ `-ui-design-assets/*.svg`), que é a fonte de verdade do port — o implementador porta classe-a-classe. Indisponibilidade/quota desce a ladder — `opencode-go/deepseek-v4.1-flash` (agente `designer-degraded`) → `deepseek/deepseek-flash` (inline pelo orquestrador) — sempre gravando o tier no PR (`Design tier:`); tier degradado marca `DEGRADED`, não certifica e para em sign-off humano (triggers no work-issue: OPS115). Ambos têm `permission` fail-closed: `edit` negado fora de `docs/plans/*-ui-design*` e `bash` com os **vetores de escrita enumerados** negados (redirecionamento, `sed -i`, `tee`, shells/interpretadores — OPS117), preservando leitura e o `prettier --write` do artefato; o resto do shell fica em `ask` (o guard cobre os vetores, não é sandbox de SO).
|
||||
|
||||
- **Agente faz sozinho:** claim (pool-supervisor no pool; humano via `pnpm agent:claim` fora da sessão — OPS33: `worktree next` claima) → implementa → **e2e local afetado** (OPS72, discricionário: roda os e2e que criou + os da mesma superfície — `pnpm test:e2e:affected`; ver skill) → **`pnpm push`** (origin = GitHub) → PR via `node scripts/github-pr.mjs` **Ready** (nunca draft) com `Closes #N` → o safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo do GitHub** (`enablePullRequestAutoMerge`, rebase) — o servidor mergea quando o required check `CI (PR) / checks` fica verde. Regra always-on: `.agents/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false` (o default draft da tool **não** vale neste repo) — o merge continua pelo safety net.
|
||||
- **Só humano:** aprovar o environment `production` no run (o único gate de publicação — OPS104 nunca auto-aprova; OPS107: aprove só o run escolhido e rejeite os demais — runs não aprovados não publicam nada), envs de produção no homeserver (`~/stack/teqo-1313.env`, fora do repo — o deploy as lê via BuildKit secrets), `GITHUB_TOKEN` local (PAT) para os scripts GitHub, secrets no repo GitHub, `pnpm configure:branch-protection` (idempotente; reaplicar se o drift voltar), runbook manual/rollback (`docs/ops/teqo-1313-deploy.md`).
|
||||
|
|
|
|||
1
docs/changelog/2026-09-16-ops117.md
Normal file
1
docs/changelog/2026-09-16-ops117.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**OPS117 — Guard de escrita dos agentes de design além dos file tools (bash) (2026-09-16):** fecha o resíduo declarado no OPS113 — os agentes `designer` e `designer-degraded` eram fail-closed nos file tools (`edit`/`write`/`patch`), mas a escrita via **bash** (redirecionamento, `sed -i`, `tee`) escapava do gate. Ambos os frontmatters ganham o mesmo bloco `permission.bash`: `'*': ask` + deny dos vetores de escrita (`*>*`/`>>`, editores in-place `sed`, mutadores `cp/mv/rm/mkdir/touch/truncate/dd/install/ln/chmod/chown/tee`, shells/interpretadores `bash/sh/zsh/python/node/bun/deno/perl/ruby`, `find -exec/-delete`, `xargs`, `git checkout/restore/apply`) e, **por último** (`last-match-wins`), o allow do artefato (`* > docs/plans/*-ui-design*`, `prettier --write docs/plans/*-ui-design*`). Evidência do mecanismo: o tool bash do opencode 1.18.31 resolve `permission.bash` pelo source tree-sitter do comando e, em redirecionamento, casa o `redirected_statement` inteiro **incluindo o caminho de destino**; smoke em subprocesso `opencode run --auto --agent designer` provou `echo x > src/…` negado (arquivo ausente), artefato permitido, `sed -i`/`tee`/`printf >`/`bash -c`/`python3 -c` negados e `cat` (leitura) não bloqueada. Novo `tests/unit/opencodeAgents.unit.spec.ts` (anti-drift: os dois frontmatters têm o bloco `bash` idêntico, o allow do artefato é a última regra e o `edit` fail-closed do OPS113 segue intacto). 1 nota na doutrina `.agents/skills/plan-issue/ui-design-html.md` e 1 frase em `docs/AGENT-OPS.md`. Resíduo honesto declarado: não é sandbox de SO — sob `--auto` um vetor exótico fora da deny-list ainda poderia passar; sem migration, sem schema e sem `src/`. Verificação: `opencode debug agent` (ruleset resolvido) + smoke subprocesso, `pnpm gate:fast`; Closes #1063.
|
||||
145
docs/plans/ops117-guard-bash-agentes-design-impl.md
Normal file
145
docs/plans/ops117-guard-bash-agentes-design-impl.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# 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 `designer` e `designer-degraded` fora de `docs/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 em `src/`.
|
||||
- **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.bash` casa comando, não caminho — a regra por caminho pode não existir") **já foi resolvida pela evidência**: com redirecionamento o glob casa o `redirected_statement` inteiro, **incluindo o destino do redirect**, logo uma regra de artefato por caminho é viável (e `last-match-wins` permite o allow depois do deny). O risco "semântica por caminho não existir" está morto.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
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)
|
||||
|
||||
1. **Forma do guard.**
|
||||
- Opções: A) declarativa `permission.bash` por agente | B) plugin com hook | C) default-deny + allowlist.
|
||||
- Recomendação: **A**. Bloco **idêntico** nos dois frontmatters, sob o `permission` existente:
|
||||
```yaml
|
||||
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-place `sed` (`sed *-i*` casa `-i` e `--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ência `bash -c` e `python3 -c`); (5) destrutivos de git (`checkout/restore/apply`) e `find -exec/-delete`/`xargs`. **Fora da lista (permanecem `ask`, auto-aprovados em `--auto` e 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 `'*': ask` fica explícito (baseline do ruleset e merge com o global `bash: 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).
|
||||
|
||||
2. **Teste unit anti-drift (barato, incluído).**
|
||||
- Opções: A) `tests/unit/opencodeAgents.unit.spec.ts` lê os dois `.md` e exige o bloco `bash` idê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.ts` já lê `.opencode/*.md` sem app/DB. Extrai o sub-bloco `bash:` do frontmatter por regex (sem dependência de YAML) e afirma: existe nos dois, é **idêntico**, contém `'*': ask`, `'*>*': deny` e o allow `* > docs/plans/*-ui-design*`; o bloco `edit` segue fail-closed (`'*': deny` + allow do artefato). Roda no `test:unit` do `gate: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.
|
||||
|
||||
3. **Docs (toque mínimo, sem twinar).**
|
||||
- Opções: A) 1 nota em `ui-design-html.md` + 1 frase em `docs/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 `permission` cobre **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.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`.opencode/agent/designer.md`** / **`.opencode/agent/designer-degraded.md`**: adicionar o bloco `bash` da decisão 1 sob o `permission` (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.md`** e **`docs/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
|
||||
|
||||
1. **F1 — Guard declarativo** (~40%): bloco `bash` idêntico nos dois agentes; `opencode debug agent designer` (subprocesso novo — config não é hot-reload) mostra o ruleset resolvido (`*>* deny`, artifact allow).
|
||||
2. **F2 — Teste + docs** (~30%): `tests/unit/opencodeAgents.unit.spec.ts` verde; 1 frase em `ui-design-html.md` e em `AGENT-OPS.md:24`; changelog.
|
||||
3. **F3 — Verificação** (~30%): smokes em subprocesso; `pnpm exec prettier --check` nos tocados; `pnpm gate:fast`; `git diff --stat` sem `src/`; `pnpm push` → PR `Closes #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 prova `echo x > docs/plans/<slug>-ui-design.html` permitido; 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 pelo `opencode debug agent`.
|
||||
- **Prettier/eslint em `.ts` novo e em `.opencode/`:** rodar `prettier --write` nos tocados; `.opencode/` não é ignorado (`.prettierignore` ignora `.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.html` e `src/ops117-smoke.txt` antes do commit.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] 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/`.
|
||||
- [x] 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.
|
||||
- [x] 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 de `sed -i` com 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.md` e 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", `bash` em backticks no AGENT-OPS.
|
||||
- **Absorvido:** status do plano `em execução` → `entregue`, coerente com os aceites `[x]`.
|
||||
- **Descartados:** `designer-campanha-solla.md` sem `permission` — é persona designer **e implementador** (constrói a página de campanha; precisa escrever, inclusive em `src/`), fora da família OPS113/117; um guard fail-closed o quebraria (não é débito). Resíduo do `--auto` sob 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 regras `bash` idênticas (32 do agente + o `ask` global), `edit` fail-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 -c` negados; `cat`/`ls` (leitura) não bloqueados. `designer-degraded` via task: mesmo deny. Artefatos de smoke removidos.
|
||||
- `pnpm gate:fast` verde (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.
|
||||
92
tests/unit/opencodeAgents.unit.spec.ts
Normal file
92
tests/unit/opencodeAgents.unit.spec.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
// OPS117 guard: the two design agents must carry the SAME `permission.bash`
|
||||
// ruleset. They are two files by design (the degraded agent is a distinct
|
||||
// `mode: subagent` identity), so nothing but this spec stops one of them from
|
||||
// drifting while the other keeps the guard — the exact anti-goal (`designer`
|
||||
// guarded, `designer-degraded` leaking a shell write).
|
||||
//
|
||||
// The expected array is pinned on purpose: comparing the two files only would
|
||||
// let a rule disappear from BOTH at once. Here the contract is the literal
|
||||
// ruleset, so eroding the guard fails the build too.
|
||||
//
|
||||
// The spec parses the YAML frontmatter line-by-line (no YAML dependency): a
|
||||
// block is the 4-space-indented run under ` <key>:`. Order matters — bash
|
||||
// rules resolve last-match-wins, so the artifact allow sits at the end.
|
||||
|
||||
const repoRoot = process.cwd()
|
||||
const agents = ['designer', 'designer-degraded'] as const
|
||||
|
||||
// Keep in sync with `.opencode/agent/designer.md` and `designer-degraded.md`.
|
||||
const expectedBash = [
|
||||
`'*': 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`,
|
||||
]
|
||||
|
||||
const frontmatter = (agent: string) => {
|
||||
const content = readFileSync(resolve(repoRoot, `.opencode/agent/${agent}.md`), 'utf8')
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---/)
|
||||
expect(match, `${agent}: frontmatter must be delimited by ---`).not.toBeNull()
|
||||
return match![1]
|
||||
}
|
||||
|
||||
const block = (agent: string, key: 'bash' | 'edit') => {
|
||||
const lines = frontmatter(agent).split('\n')
|
||||
const start = lines.findIndex((line) => line === ` ${key}:`)
|
||||
expect(start, `${agent}: permission.${key} block must exist`).toBeGreaterThanOrEqual(0)
|
||||
|
||||
const rules: string[] = []
|
||||
for (const line of lines.slice(start + 1)) {
|
||||
if (line.trim() === '') continue
|
||||
if (!line.startsWith(' ')) break
|
||||
rules.push(line.trim())
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
describe('design agents share the OPS117 bash write guard', () => {
|
||||
it.each(agents)('%s declares the pinned bash ruleset (default ask, writes denied)', (agent) => {
|
||||
const rules = block(agent, 'bash')
|
||||
|
||||
// Default is `ask` (never silent), the artifact is the last match, and the
|
||||
// enumerated write vectors — redirect, in-place sed, tee, mutators, nested
|
||||
// shells/interpreters, find -exec/-delete, xargs, destructive git — deny.
|
||||
expect(rules).toEqual(expectedBash)
|
||||
})
|
||||
|
||||
it.each(agents)('%s keeps the file-tool gate fail-closed (OPS113 intact)', (agent) => {
|
||||
expect(block(agent, 'edit')).toEqual([`'*': deny`, `'docs/plans/*-ui-design*': allow`])
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue