agent-engineering-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +249 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.js +223 -0
- package/docs/agent-integration.md +200 -0
- package/docs/philosophy.md +131 -0
- package/docs/reference-authoring.md +117 -0
- package/docs/skill-authoring.md +126 -0
- package/examples/authorization-bypass.md +191 -0
- package/examples/frontend-review.md +244 -0
- package/examples/race-condition.md +128 -0
- package/examples/xp-reward-loop.md +123 -0
- package/package.json +45 -0
- package/references/engineering.yaml +88 -0
- package/references/frontend.yaml +139 -0
- package/references/product.yaml +54 -0
- package/references/research.yaml +88 -0
- package/references/security.yaml +85 -0
- package/references/ux.yaml +37 -0
- package/scripts/validate.py +454 -0
- package/skills/audit/adversarial-review/SKILL.md +190 -0
- package/skills/audit/business-logic-audit/SKILL.md +182 -0
- package/skills/audit/edge-case-hunter/SKILL.md +159 -0
- package/skills/audit/error-flow-audit/SKILL.md +184 -0
- package/skills/audit/state-consistency-audit/SKILL.md +174 -0
- package/skills/audit/user-flow-audit/SKILL.md +161 -0
- package/skills/frontend/accessibility-review/SKILL.md +186 -0
- package/skills/frontend/animation-review/SKILL.md +171 -0
- package/skills/frontend/interaction-design/SKILL.md +162 -0
- package/skills/frontend/ux-review/SKILL.md +172 -0
- package/skills/frontend/visual-quality-review/SKILL.md +160 -0
- package/skills/meta/research-router/SKILL.md +184 -0
- package/skills/meta/skill-router/SKILL.md +206 -0
- package/skills/product/gamification-audit/SKILL.md +213 -0
- package/skills/reliability/data-integrity-audit/SKILL.md +187 -0
- package/skills/reliability/idempotency-audit/SKILL.md +191 -0
- package/skills/reliability/race-condition-hunter/SKILL.md +181 -0
- package/skills/research/github-reference-research/SKILL.md +197 -0
- package/skills/research/implementation-research/SKILL.md +181 -0
- package/skills/research/market-research/SKILL.md +202 -0
- package/skills/research/reference-research/SKILL.md +186 -0
- package/skills/security/api-abuse-audit/SKILL.md +178 -0
- package/skills/security/authorization-audit/SKILL.md +176 -0
- package/skills/security/input-trust-audit/SKILL.md +178 -0
- package/templates/audit-report.md +89 -0
- package/templates/bug-report.md +107 -0
- package/templates/design-review.md +122 -0
- package/templates/research-report.md +96 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-integrity-audit
|
|
3
|
+
description: Verifies unique constraints, foreign keys, transactions, cascading, soft delete, enums, and database constraints to confirm the database prevents impossible states instead of trusting application logic to do so.
|
|
4
|
+
category: reliability
|
|
5
|
+
triggers:
|
|
6
|
+
- "audit data integrity"
|
|
7
|
+
- "check database constraints"
|
|
8
|
+
- "unique constraint and foreign keys"
|
|
9
|
+
- "soft delete and orphaned data"
|
|
10
|
+
- "prevent impossible states at the db"
|
|
11
|
+
priority: high
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Data Integrity Audit
|
|
15
|
+
|
|
16
|
+
## Objective
|
|
17
|
+
|
|
18
|
+
Ensinar o agente a verificar se o **banco de dados** impede estados impossíveis —
|
|
19
|
+
em vez de confiar que a aplicação nunca vai gravá-los. A pergunta central (do
|
|
20
|
+
`plan.md` §8):
|
|
21
|
+
|
|
22
|
+
> "O banco deve impedir estados impossíveis sempre que apropriado."
|
|
23
|
+
|
|
24
|
+
Campos em foco: unique constraints, foreign keys, transactions, cascading, soft delete,
|
|
25
|
+
enums, database constraints.
|
|
26
|
+
|
|
27
|
+
## When to Use
|
|
28
|
+
|
|
29
|
+
* Quando a aplicação grava dados e *assume* invariants que só ela conhece (regra de
|
|
30
|
+
negócio) — se a regra não está no banco, outra rota pode violá-la.
|
|
31
|
+
* Em fluxos com unicidade (username, slug, chave de idempotência), referências
|
|
32
|
+
(FK), estados (enum), e soft delete.
|
|
33
|
+
* Quando há escrita concorrente ou múltiplos writers (jobs, workers, admin, import)
|
|
34
|
+
fora do fluxo da aplicação principal.
|
|
35
|
+
* Quando o pedido menciona "data integrity", "constraints", "unique", "foreign key",
|
|
36
|
+
"soft delete", "orphans", "enums", "transactions".
|
|
37
|
+
* **Composição:** pareia com `race-condition-hunter` (constraint = defesa contra
|
|
38
|
+
race), `idempotency-audit` (unique na chave de idempotência), `error-flow-audit`
|
|
39
|
+
(transação/rollback), `business-logic-audit` (invariants que deveriam ser constraints),
|
|
40
|
+
`state-consistency-audit` (banco como fonte de verdade).
|
|
41
|
+
|
|
42
|
+
## Mental Model
|
|
43
|
+
|
|
44
|
+
A aplicação é uma via de escrita; o banco é a **última linha de defesa**. Toda regra
|
|
45
|
+
que impede um estado impossível deve, idealmente, existir como constraint — porque:
|
|
46
|
+
|
|
47
|
+
1. **Aplicação não é o único writer** — jobs, workers, imports, admin tools, migrações,
|
|
48
|
+
e correções manuais escrevem fora dos handlers. Só o banco cobre todos.
|
|
49
|
+
2. **Aplicação pode errar** — um bug de lógica grava dados inválidos e não há erro.
|
|
50
|
+
A constraint transforma silêncio em erro.
|
|
51
|
+
3. **Aplicação pode ter race** — dois requests passam a checagem; a constraint rejeita
|
|
52
|
+
o segundo (ver `race-condition-hunter`).
|
|
53
|
+
|
|
54
|
+
A skill verifica, para cada invariant importante: **ele está no banco, ou só na
|
|
55
|
+
aplicação?** Se só na aplicação, é uma crença, não uma garantia.
|
|
56
|
+
|
|
57
|
+
Eixos a verificar (do `plan.md` §8):
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
unique constraints — unicidade real ou contornável?
|
|
61
|
+
foreign keys — referências pendentes/órfãs?
|
|
62
|
+
transactions — writes agrupados atomically?
|
|
63
|
+
cascading — o que acontece quando o pai some?
|
|
64
|
+
soft delete — registros "deletados" ainda referenciáveis?
|
|
65
|
+
enums — estados fora do domínio permitidos?
|
|
66
|
+
database constraints — CHECK, NOT NULL, DEFAULT, tipos corretos?
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Investigation Procedure
|
|
70
|
+
|
|
71
|
+
1. **Inventariar invariants** do schema — unicidade, obrigatoriedade, domínio de
|
|
72
|
+
estados, referências, relação pai-filho.
|
|
73
|
+
2. **Para cada, verificar se há constraint real no banco** (migração/schema), não só
|
|
74
|
+
validação de aplicação.
|
|
75
|
+
3. **Unique**: a coluna é única no DB? Ou só verificada no handler? (race gap)
|
|
76
|
+
4. **FK**: referências têm FK real com `ON DELETE` definido? (RESTRICT/CASCADE/SET NULL)
|
|
77
|
+
5. **Transações**: operações multi-write são atômicas (uma transaction), ou writes
|
|
78
|
+
parciais possíveis?
|
|
79
|
+
6. **Soft delete**: registros soft-deleted ainda são referenciáveis/recuperáveis sem
|
|
80
|
+
proteção? A unicidade ainda vale entre "deletado" e "ativo"?
|
|
81
|
+
7. **Enums**: estados são `CHECK`/`enum` no DB, ou strings livres na aplicação?
|
|
82
|
+
Valores fora do domínio podem ser gravados?
|
|
83
|
+
8. **Constraints de domínio**: NOT NULL, CHECK (ex: saldo ≥ 0), DEFAULT, tipos.
|
|
84
|
+
9. **Testar**: tente inserir/atualizar um valor que violaria o invariant *direto no
|
|
85
|
+
banco* (ou via aplicação sem validação). O banco rejeita?
|
|
86
|
+
10. **Confirmar com evidência** e reportar via `templates/audit-report.md`.
|
|
87
|
+
|
|
88
|
+
## Questions to Ask
|
|
89
|
+
|
|
90
|
+
* Cada invariant importante tem constraint no banco, ou só validação na aplicação?
|
|
91
|
+
* Unique: há índice único? Ou duplicata é possível via race/outro writer?
|
|
92
|
+
* FK: há constraint real? O `ON DELETE` está definido e correto?
|
|
93
|
+
* Soft delete: a unicidade separa "deletado" de "ativo"? Um slug deletado pode ser
|
|
94
|
+
reclamado? Um item soft-deleted ainda é editável/referenciável?
|
|
95
|
+
* Enums: o estado é validado pelo banco (CHECK/enum) ou string livre?
|
|
96
|
+
* CHECK: o banco rejeita `balance < 0`, `quantity < 0`, `date` inválido?
|
|
97
|
+
* Multi-write: as operações são atômicas? Uma falha no meio deixa partial?
|
|
98
|
+
* Migrações: constraints existem no schema de produção, não só em algum ambiente?
|
|
99
|
+
* Outros writers (jobs/admin/import) respeitam as regras que só a aplicação conhece?
|
|
100
|
+
|
|
101
|
+
## Attack Patterns
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
unique bypass
|
|
105
|
+
app: verifica username único no handler
|
|
106
|
+
banco: sem unique index
|
|
107
|
+
→ race ou outro writer grava duplicata; unicidade violada silenciosamente
|
|
108
|
+
|
|
109
|
+
orphan FK
|
|
110
|
+
post.deleted (hard) → comments.post_id pendente
|
|
111
|
+
sem FK / ON DELETE RESTRICT
|
|
112
|
+
→ comentários órfãos, referência inválida
|
|
113
|
+
|
|
114
|
+
soft delete + unique collision
|
|
115
|
+
"deleted" é só um flag; unique na coluna `slug`
|
|
116
|
+
→ slug deletado bloqueia reuso, OU slug reusado cria duplicata visível
|
|
117
|
+
(defesa: unique parcial `WHERE deleted_at IS NULL`)
|
|
118
|
+
|
|
119
|
+
enum as free string
|
|
120
|
+
estado gravado como string no app ("paid", "paid2")
|
|
121
|
+
banco: VARCHAR, sem CHECK
|
|
122
|
+
→ estados fora do domínio persistidos; transições inválidas viram dado
|
|
123
|
+
|
|
124
|
+
no CHECK on negative
|
|
125
|
+
banco aceita quantity = -5
|
|
126
|
+
app valida, mas import/job/admin grava -5
|
|
127
|
+
→ estado impossível no dado
|
|
128
|
+
|
|
129
|
+
transaction partial
|
|
130
|
+
writes em 3 tabelas sem transaction
|
|
131
|
+
falha na 2ª → 1ª commitada, 2ª e 3ª não
|
|
132
|
+
→ estado parcial (overlap com error-flow-audit)
|
|
133
|
+
|
|
134
|
+
cascade wrong
|
|
135
|
+
DELETE user → CASCADE apaga posts (talvez certo) ou RESTRICT bloqueia por um
|
|
136
|
+
comment órfão (talvez errado)
|
|
137
|
+
→ política ON DELETE ausente ou errada
|
|
138
|
+
|
|
139
|
+
NULL where not allowed
|
|
140
|
+
sem NOT NULL em coluna obrigatória (ownerId)
|
|
141
|
+
→ registro sem dono; depois autorização quebra (overlap com authorization-audit)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Evidence Requirements
|
|
145
|
+
|
|
146
|
+
* **Nomear o invariant e o eixo** (unique/FK/transaction/soft-delete/enum/CHECK).
|
|
147
|
+
* **Mostrar o schema real** — a migração/DDL que (não) tem a constraint. Cite o
|
|
148
|
+
arquivo/schema.
|
|
149
|
+
* **Mostrar a violação** — inserção/atualização que o banco *aceita* mas que viola o
|
|
150
|
+
invariant. Ou a demonstração de que ela é possível (outro writer, race).
|
|
151
|
+
* **Escalar confiança:**
|
|
152
|
+
* `CONFIRMED` — gravou o dado impossível direto no banco (ou provou o caminho que o
|
|
153
|
+
grava).
|
|
154
|
+
* `HIGH CONFIDENCE` — schema claramente sem a constraint em invariant claro.
|
|
155
|
+
* `POSSIBLE` — invariant assumido, ausência de constraint plausível.
|
|
156
|
+
* `SPECULATIVE` — "deveria ter constraint" sem identificar o invariant concreto.
|
|
157
|
+
* Ausência de unique em unicidade real de negócio = mínimo `HIGH CONFIDENCE`.
|
|
158
|
+
|
|
159
|
+
## False Positives
|
|
160
|
+
|
|
161
|
+
* **Constraint existe em migração** — o índice/CHECK/FK existe no schema de produção;
|
|
162
|
+
a validação de app é defesa adicional. Confirmar no DDL real antes de reportar.
|
|
163
|
+
* **Semanticamente não-único** — se a coluna *não deveria* ser única (ex: nomes
|
|
164
|
+
repetem), "sem unique" não é bug. Verificar o invariant de negócio.
|
|
165
|
+
* **Soft delete sem reuso** — se o produto *não* reusa slugs/identificadores de
|
|
166
|
+
deletados, unique parcial é desnecessário. Julgar pelo comportamento desejado.
|
|
167
|
+
* **Enums de app suficientes** — se todos os writers passam pela mesma validação de
|
|
168
|
+
enum da aplicação (nunca há outros writers), string livre é aceitável. Raro; verificar
|
|
169
|
+
a existência de outros writers.
|
|
170
|
+
* **CHECK redundante** — se a app garante `balance ≥ 0` com lock+transaction e não há
|
|
171
|
+
outro writer, o CHECK é reforço, não necessidade. Marcar como melhoria, não bug.
|
|
172
|
+
* **Cascade policy é correta para o domínio** — CASCADE em user→posts pode ser
|
|
173
|
+
intencional (dados sem valor pós-exclusão). Não reportar política correta.
|
|
174
|
+
|
|
175
|
+
## Output Format
|
|
176
|
+
|
|
177
|
+
Para cada invariant sem constraint no banco, um finding via `templates/audit-report.md`.
|
|
178
|
+
Em **Affected component**, nomeie a tabela/coluna e o DDL. Em **Reproduction**, dê a
|
|
179
|
+
inserção/atualização que viola o invariant e mostre que o banco aceita. Em **Root
|
|
180
|
+
cause**, diga qual constraint falta (unique/FK/CHECK/enum/transaction/NOT NULL). Em
|
|
181
|
+
**Recommendation**, indique a constraint exata (ex: `UNIQUE (user_id, target_id)`,
|
|
182
|
+
`CHECK (balance >= 0)`, `FOREIGN KEY ... ON DELETE RESTRICT`, `CHECK` no enum) e a
|
|
183
|
+
migração.
|
|
184
|
+
|
|
185
|
+
Apresente a tabela por invariant (invariant | eixo | constraint no banco? | onde é
|
|
186
|
+
aplicado hoje | ✓/✗). Unique e FK (dados órfãos/duplicados) primeiro; soft delete e
|
|
187
|
+
enums depois; CHECK/transações em seguida.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: idempotency-audit
|
|
3
|
+
description: Tests request → request → request and request → response-lost → retry, especially for payments, rewards, creation, webhooks, notifications, and counters, to find operations that duplicate effects on repeat or retry.
|
|
4
|
+
category: reliability
|
|
5
|
+
triggers:
|
|
6
|
+
- "audit idempotency"
|
|
7
|
+
- "duplicate effects on retry"
|
|
8
|
+
- "double submit double charge"
|
|
9
|
+
- "webhook replay"
|
|
10
|
+
- "request repeated N times"
|
|
11
|
+
priority: high
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Idempotency Audit
|
|
15
|
+
|
|
16
|
+
## Objective
|
|
17
|
+
|
|
18
|
+
Ensinar o agente a testar se operações **idempotentes-deveria-ser** continuam
|
|
19
|
+
produzindo efeito único quando repetidas ou reexecutadas após falha/retry:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
request
|
|
23
|
+
request
|
|
24
|
+
request ← mesmo efeito 1×? ou efeito duplicado N×?
|
|
25
|
+
|
|
26
|
+
request
|
|
27
|
+
response lost
|
|
28
|
+
retry ← o retry duplica o efeito que já aconteceu?
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Especialmente em (do `plan.md` §8): **pagamentos, rewards, criação, webhooks,
|
|
32
|
+
notificações, contadores.**
|
|
33
|
+
|
|
34
|
+
## When to Use
|
|
35
|
+
|
|
36
|
+
* Em qualquer operação que *cria* algo, *concede* algo, *debita/cobra* algo, ou
|
|
37
|
+
*incrementa* — se repetida, duplicaria.
|
|
38
|
+
* Em endpoints que recebem webhooks/retries de terceiros (gateway de pagamento,
|
|
39
|
+
provedores).
|
|
40
|
+
* Quando o usuário pode duplo-submitar (duplo clique, retry manual).
|
|
41
|
+
* Quando o pedido menciona "idempotency", "duplicate", "double charge", "double
|
|
42
|
+
submit", "webhook replay", "retry".
|
|
43
|
+
* **Composição:** pareia com `error-flow-audit` (retry após falha mid-op),
|
|
44
|
+
`race-condition-hunter` (retry concorrente), `gamification-audit` (reward duplicada),
|
|
45
|
+
`api-abuse-audit` (repetição via API), `business-logic-audit` (regra de efeito único).
|
|
46
|
+
|
|
47
|
+
## Mental Model
|
|
48
|
+
|
|
49
|
+
Idempotência não é "aceitar o request duas vezes". É **produzir o mesmo efeito** na
|
|
50
|
+
segunda vez. Um POST que cria um pedido é idempotente só se reenviá-lo (mesma
|
|
51
|
+
idempotency key, mesmo payload) retorna o pedido original sem criar outro.
|
|
52
|
+
|
|
53
|
+
O que torna um request *candidato* a idempotente: existe uma **chave de idempotência**
|
|
54
|
+
(um identificador estável da intenção — ex: `Idempotency-Key` header, `orderId`,
|
|
55
|
+
`eventId` de webhook) e o servidor **verifica a chave** antes de executar. Sem chave
|
|
56
|
+
verificada, a repetição duplica.
|
|
57
|
+
|
|
58
|
+
Eixo de investigação:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
qual é a chave de idempotência? (se não existe, suspeito)
|
|
62
|
+
o servidor verifica antes de agir? (lookup por chave antes do efeito)
|
|
63
|
+
o efeito é duplicado no retry? (quando a chave não é verificada, ou a resposta se perdeu)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Áreas críticas (efeito duplicado = consequência real):
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
payments — cobrança duplicada
|
|
70
|
+
rewards — XP/pontos concedidos 2×
|
|
71
|
+
creation — recurso criado 2×
|
|
72
|
+
webhooks — evento processado 2× (sem eventId dedup)
|
|
73
|
+
notifications — email/SMS duplicado
|
|
74
|
+
counters — incremento dobrado
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Investigation Procedure
|
|
78
|
+
|
|
79
|
+
1. **Listar operações que criam/concedem/debitam/incrementam.**
|
|
80
|
+
2. **Para cada, identificar a chave de idempotência natural** — existe no request
|
|
81
|
+
(header, idempotency key, eventId) ou no payload?
|
|
82
|
+
3. **Rastrear onde a chave é verificada** — o handler busca o efeito pela chave antes
|
|
83
|
+
de executar? Ou executa sempre?
|
|
84
|
+
4. **Testar repetição** — envie o mesmo request N vezes (mesma chave/payload). Efeito
|
|
85
|
+
único ou duplicado?
|
|
86
|
+
5. **Testar response-lost + retry** — envie, ignore a resposta, reenvie. O servidor
|
|
87
|
+
reconhece e retorna o efeito original, ou executa de novo?
|
|
88
|
+
6. **Testar duplo-submit** — dois POSTs no mesmo instante (double click). Um 200 e um
|
|
89
|
+
409, ou dois 200 com dois efeitos?
|
|
90
|
+
7. **Testar webhook dedup** — o mesmo evento entregue 2× (retry do provider) é
|
|
91
|
+
processado 2×?
|
|
92
|
+
8. **Testar concorrência de idempotência** — dois requests com a mesma chave chegam
|
|
93
|
+
juntos; ambos passam a verificação antes de qualquer um gravar? (race no dedup)
|
|
94
|
+
9. **Confirmar com evidência** — reproduza a duplicação.
|
|
95
|
+
10. **Reportar** via `templates/audit-report.md`.
|
|
96
|
+
|
|
97
|
+
## Questions to Ask
|
|
98
|
+
|
|
99
|
+
* Qual é a chave de idempotência desta operação? Ela existe?
|
|
100
|
+
* O servidor verifica a chave antes de executar, ou executa e só então grava?
|
|
101
|
+
* Enviar o mesmo request 2× — efeito único ou duplicado?
|
|
102
|
+
* Resposta perdida + retry — o servidor reconhece ou reexecuta?
|
|
103
|
+
* Double click / duplo submit — dois efeitos?
|
|
104
|
+
* Webhook reentregue pelo provider (retry) — processado 2×?
|
|
105
|
+
* A verificação de idempotência é atômica (lock/unique na chave), ou dois requests com
|
|
106
|
+
a mesma chave podem passar juntos?
|
|
107
|
+
* O efeito é registrado *depois* de um efeito externo irreversível (cobrança)? (se sim,
|
|
108
|
+
conecta a `error-flow-audit`)
|
|
109
|
+
|
|
110
|
+
## Attack Patterns
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
creation duplicated
|
|
114
|
+
POST /orders → 200 (pedido A)
|
|
115
|
+
POST /orders (mesmo payload) → 200 (pedido B) ← dois pedidos, deveria reusar A
|
|
116
|
+
|
|
117
|
+
payment double charge
|
|
118
|
+
POST /charge {amount, orderId} → 200 (cobrado)
|
|
119
|
+
retry (response lost) → 200 (cobrado de novo) ← sem idempotency key verificado
|
|
120
|
+
|
|
121
|
+
reward double-grant
|
|
122
|
+
POST /react → +10 XP
|
|
123
|
+
replay → +10 XP de novo (mesmo target) ← sem "already reacted" dedup
|
|
124
|
+
|
|
125
|
+
webhook reprocessing
|
|
126
|
+
provider: eventId=evt_123 entregue → processado (pedido pago)
|
|
127
|
+
provider retry (sem ACK): evt_123 de novo → processado 2× (recompensa 2×)
|
|
128
|
+
← falta dedup por eventId
|
|
129
|
+
|
|
130
|
+
notification duplicate
|
|
131
|
+
POST /notify → email enviado
|
|
132
|
+
retry → email de novo ← sem dedup por recipient+type+id
|
|
133
|
+
|
|
134
|
+
counter double increment
|
|
135
|
+
POST /increment (mesma chave) ×2 → count += 2 ← deveria += 1
|
|
136
|
+
|
|
137
|
+
race on idempotency key
|
|
138
|
+
A e B: POST {key:"k1"} (nenhum gravou ainda)
|
|
139
|
+
ambos passam lookup (não acham k1)
|
|
140
|
+
ambos executam → efeito duplicado apesar da chave
|
|
141
|
+
defesa: unique constraint na chave (segundo INSERT falha)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Evidence Requirements
|
|
145
|
+
|
|
146
|
+
* **Nomear a operação e sua chave de idempotência** (ou a ausência dela).
|
|
147
|
+
* **Mostrar o efeito duplicado** — repetição/retry/replay reproduzido, com o efeito 1×
|
|
148
|
+
vs 2×.
|
|
149
|
+
* **Mostrar onde a chave não é verificada** — o handler executa sem lookup por chave,
|
|
150
|
+
ou verifica depois de agir.
|
|
151
|
+
* **Verificar a atomicidade do dedup** — a chave tem unique constraint? Ou dois
|
|
152
|
+
requests simultâneos passam juntos (race)?
|
|
153
|
+
* **Escalar confiança:**
|
|
154
|
+
* `CONFIRMED` — reproduziu o efeito duplicado (2 pedidos, 2 cobranças, 2 rewards).
|
|
155
|
+
* `HIGH CONFIDENCE` — handler executa sem verificação de chave em operação de
|
|
156
|
+
efeito-único; sem reprodução.
|
|
157
|
+
* `POSSIBLE` — operação candidata, caminho não confirmado.
|
|
158
|
+
* `SPECULATIVE` — "pode duplicar em retry" sem rastrear.
|
|
159
|
+
* Duplicação de cobrança/reward = mínimo `HIGH CONFIDENCE` se o padrão for claro.
|
|
160
|
+
|
|
161
|
+
## False Positives
|
|
162
|
+
|
|
163
|
+
* **Idempotência real** — se há idempotency key + lookup + unique constraint, a
|
|
164
|
+
repetição retorna o efeito original. Confirmar antes de reportar.
|
|
165
|
+
* **Operação é naturalmente idempotente** — `GET`, `PUT` com valor absoluto (SET), e
|
|
166
|
+
`DELETE` muitas vezes são idempotentes por natureza. Não reportar.
|
|
167
|
+
* **Duplo-submit defendido** — se o botão desabilita no submit E o servidor tem
|
|
168
|
+
idempotency, defesa em profundidade. Não reportar.
|
|
169
|
+
* **Webhook com dedup por eventId** — se o provider envia eventId e o servidor dedup
|
|
170
|
+
por ele (único), reprocessamento é evitado. Confirmar a dedup.
|
|
171
|
+
* **Notificação é fire-and-forget tolerada** — se o produto tolera email duplicado
|
|
172
|
+
raro (e não há consequência), pode ser aceitável. Julgar pelo impacto.
|
|
173
|
+
* **Contador é aproximado por design** — alguns contadores (views) são eventualmente
|
|
174
|
+
consistentes e toleram drift. Relacionado a `state-consistency-audit`; não reportar
|
|
175
|
+
se o produto tolera.
|
|
176
|
+
* **Retry com query-decidida** — se após response lost o sistema consulta o estado
|
|
177
|
+
real antes de reexecutar, é idempotente por compensação. Não reportar.
|
|
178
|
+
|
|
179
|
+
## Output Format
|
|
180
|
+
|
|
181
|
+
Para cada operação que duplica efeito em repetição/retry, um finding via
|
|
182
|
+
`templates/audit-report.md`. Em **Reproduction**, dê a sequência de requests (mesma
|
|
183
|
+
chave/payload) e o efeito observado 1× vs 2×. Em **Affected flow**, nomeie a operação
|
|
184
|
+
(payment/reward/creation/webhook/notification/counter). Em **Root cause**, diga o que
|
|
185
|
+
falta (idempotency key, lookup antes de agir, unique constraint na chave, dedup por
|
|
186
|
+
eventId). Em **Recommendation**, indique a defesa (chave + `INSERT ... ON CONFLICT`/
|
|
187
|
+
unique, lookup + reuso do efeito, dedup por eventId em webhooks).
|
|
188
|
+
|
|
189
|
+
Apresente a tabela por operação (operação | chave de idempotência | verificada antes de
|
|
190
|
+
agir? | dedup atômico? | efeito duplicado? | ✓/✗). Cobranças e rewards primeiro;
|
|
191
|
+
criação, webhooks e notificações depois; contadores por último (impacto menor).
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: race-condition-hunter
|
|
3
|
+
description: Hunts for READ → DECISION → WRITE sequences and asks what happens if another request modifies the shared state between the read and the write, exposing double-spend, over-limit grants, and counter inflation under concurrency.
|
|
4
|
+
category: reliability
|
|
5
|
+
triggers:
|
|
6
|
+
- "audit race conditions"
|
|
7
|
+
- "find concurrency bugs"
|
|
8
|
+
- "read then write check-then-act"
|
|
9
|
+
- "double-spend and over-limit under concurrency"
|
|
10
|
+
- "two simultaneous requests"
|
|
11
|
+
priority: high
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Race Condition Hunter
|
|
15
|
+
|
|
16
|
+
## Objective
|
|
17
|
+
|
|
18
|
+
Ensinar o agente a reconhecer o padrão canônico de race condition e a testar se o
|
|
19
|
+
sistema sobrevive a dois requests simultâneos sobre o mesmo estado:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
READ
|
|
23
|
+
↓
|
|
24
|
+
DECISION
|
|
25
|
+
↓
|
|
26
|
+
WRITE
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A pergunta central (do `plan.md` §8):
|
|
30
|
+
|
|
31
|
+
> "O que acontece se outro request modificar o estado entre essas operações?"
|
|
32
|
+
|
|
33
|
+
## When to Use
|
|
34
|
+
|
|
35
|
+
* Sempre que uma operação lê estado, decide baseada no que leu, e escreve — sem lock ou
|
|
36
|
+
atomicidade.
|
|
37
|
+
* Em fluxos com valor: saldo, estoque, cota/limite, contador, reward, voto, convite.
|
|
38
|
+
* Quando há "check-then-act": checa saldo → debita; checa limite → concede; checa
|
|
39
|
+
unicidade → cria.
|
|
40
|
+
* Quando o pedido menciona "race condition", "concurrency", "simultaneous", "double
|
|
41
|
+
spend", "TOCTOU", "check then act".
|
|
42
|
+
* **Composição:** pareia com `idempotency-audit` (retry concorrente), `business-logic-audit` (limite/invariant que dependem de read-then-write), `data-integrity-audit`
|
|
43
|
+
(constraint/lock que deve defender), `error-flow-audit` (estado parcial após falha
|
|
44
|
+
concorrente), `gamification-audit` (reward farming concorrente).
|
|
45
|
+
|
|
46
|
+
## Mental Model
|
|
47
|
+
|
|
48
|
+
Uma race condition não é sobre *velocidade* — é sobre **janela entre leitura e escrita**.
|
|
49
|
+
Se dois requests leem o mesmo estado (ambos veem "ok"), ambos decidem "permitido", e
|
|
50
|
+
ambos escrevem, o invariant é violado — mesmo que cada um isoladamente esteja correto.
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
request A: READ balance=100 (≥50? yes)
|
|
54
|
+
request B: READ balance=100 (≥50? yes) ← mesma leitura, A ainda não commitou
|
|
55
|
+
request A: WRITE balance=50
|
|
56
|
+
request B: WRITE balance=50 ← saldo real = 50, mas dois débitos de 50 = -50 efetivo
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
O defeito é a **ausência de atomicidade** entre read e write. As defesas:
|
|
60
|
+
* **lock** (pessimistic) — ninguém lê/escreve entre;
|
|
61
|
+
* **conditional update / CAS** (optimistic) — `UPDATE … WHERE balance=100` falha se
|
|
62
|
+
mudou;
|
|
63
|
+
* **unique constraint** — o banco impede a duplicata;
|
|
64
|
+
* **serializable transaction / SELECT FOR UPDATE** — o banco isola.
|
|
65
|
+
|
|
66
|
+
A skill procura a janela e pergunta qual defesa (se alguma) a fecha.
|
|
67
|
+
|
|
68
|
+
## Investigation Procedure
|
|
69
|
+
|
|
70
|
+
1. **Encontrar sequências READ → DECISION → WRITE.** Para cada operação que muta
|
|
71
|
+
estado baseada em leitura, mapeie as três etapas.
|
|
72
|
+
2. **Identificar o estado compartilhado** — linha de DB, contador, cache, arquivo.
|
|
73
|
+
3. **Identificar o invariant** que a decisão protege (saldo ≥ 0, limite ≤ N, único,
|
|
74
|
+
estoque ≥ 0).
|
|
75
|
+
4. **Determinar a atomicidade** — há transação? lock? CAS? constraint? ou read e write
|
|
76
|
+
em momentos separados sem proteção?
|
|
77
|
+
5. **Modelar a intercalação** — dois requests (ou mais) lêem o mesmo estado antes de
|
|
78
|
+
qualquer write. Ambos passam pela decisão?
|
|
79
|
+
6. **Confirmar com evidência** — se possível, reproduza (dois requests simultâneos,
|
|
80
|
+
ou raciocínio sobre o SQL mostrando que o `WHERE` não guarda a condição).
|
|
81
|
+
7. **Verificar a defesa** — o `UPDATE` é condicional ao valor lido? Há `SELECT FOR
|
|
82
|
+
UPDATE`? Unique constraint? Se sim, a janela está fechada.
|
|
83
|
+
8. **Reportar** via `templates/audit-report.md`.
|
|
84
|
+
|
|
85
|
+
## Questions to Ask
|
|
86
|
+
|
|
87
|
+
* A operação lê estado e depois escreve baseada no que leu? Qual o invariant?
|
|
88
|
+
* Entre o READ e o WRITE, outro request pode modificar o mesmo estado?
|
|
89
|
+
* Há transação? Ela é serializable / usa SELECT FOR UPDATE, ou só agrupa queries?
|
|
90
|
+
* O UPDATE é condicional (`WHERE balance = :lido`) ou incondicional (`SET balance = :novo`)?
|
|
91
|
+
* Há unique constraint que impediria a duplicata mesmo sem lock de aplicação?
|
|
92
|
+
* O limite/cota é checado em read-then-write, ou decrementado atomicamente?
|
|
93
|
+
* Dois requests simultâneos passam ambos pela checagem de saldo/limite/estoque?
|
|
94
|
+
* O cache é a fonte da leitura? (race entre cache e DB — conecta a `state-consistency-audit`)
|
|
95
|
+
|
|
96
|
+
## Attack Patterns
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
double-spend (balance)
|
|
100
|
+
A: READ balance=100 (ok ≥50) B: READ balance=100 (ok ≥50)
|
|
101
|
+
A: WRITE balance=50 B: WRITE balance=50
|
|
102
|
+
→ dois saques de 50 sobre saldo 100; saldo real deveria ser 0, ficou 50 (ou -50)
|
|
103
|
+
|
|
104
|
+
over-limit grant
|
|
105
|
+
A: READ claims_today=4/5 (ok) B: READ claims_today=4/5 (ok)
|
|
106
|
+
A: WRITE 5/5 + reward B: WRITE 5/5 + reward
|
|
107
|
+
→ 6/5, limite violado
|
|
108
|
+
|
|
109
|
+
duplicate creation (uniqueness check-then-insert)
|
|
110
|
+
A: SELECT (slug exists? no) B: SELECT (slug exists? no)
|
|
111
|
+
A: INSERT slug=x B: INSERT slug=x
|
|
112
|
+
→ dois criados; defesa = unique constraint (se houver)
|
|
113
|
+
|
|
114
|
+
counter inflation
|
|
115
|
+
A: READ count=10 B: READ count=10
|
|
116
|
+
A: WRITE count=11 B: WRITE count=11
|
|
117
|
+
→ deveria ser 12, ficou 11 (increment perdido)
|
|
118
|
+
defesa = UPDATE count = count + 1 (atômico)
|
|
119
|
+
|
|
120
|
+
reward double-grant (concurrent same action)
|
|
121
|
+
A: react + XP B: react (same target) + XP
|
|
122
|
+
→ se "already reacted?" check é read-then-write, ambos concedem
|
|
123
|
+
|
|
124
|
+
stock oversell
|
|
125
|
+
A: READ stock=1 (ok) B: READ stock=1 (ok)
|
|
126
|
+
A: WRITE stock=0 + order B: WRITE stock=0 + order
|
|
127
|
+
→ dois pedidos, 1 item
|
|
128
|
+
|
|
129
|
+
cache-then-db race
|
|
130
|
+
A: read cache (miss) → read DB=100 → write cache=100
|
|
131
|
+
B: write DB=50 (invalida cache?)
|
|
132
|
+
→ cache serviu 100 após DB virar 50 (state-consistency overlap)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Evidence Requirements
|
|
136
|
+
|
|
137
|
+
* **Nomear o invariant** e a sequência READ → DECISION → WRITE.
|
|
138
|
+
* **Mostrar a janela** — onde está o READ, onde o WRITE, e por que nada os atomiza.
|
|
139
|
+
* **Mostrar a intercalação** que viola o invariant (os dois requests lendo o mesmo
|
|
140
|
+
estado).
|
|
141
|
+
* **Verificar a defesa ausente** — sem CAS, sem lock, sem constraint, sem transaction
|
|
142
|
+
serializable. Ou a defesa existe mas não cobre (ex: transação sem `FOR UPDATE`).
|
|
143
|
+
* **Escalar confiança:**
|
|
144
|
+
* `CONFIRMED` — reproduziu com requests simultâneos e observou a violação (saldo
|
|
145
|
+
negativo, 6/5, duplicata).
|
|
146
|
+
* `HIGH CONFIDENCE` — código mostra read-then-write sem atomicidade em estado
|
|
147
|
+
compartilhado, invariant claro; sem reprodução manual.
|
|
148
|
+
* `POSSIBLE` — padrão presente, estado compartilhado plausível, não confirmado.
|
|
149
|
+
* `SPECULATIVE` — "pode ter race" sem mapear a sequência.
|
|
150
|
+
* Races sobre valor transferível (saldo/estoque/reward) = mínimo `HIGH CONFIDENCE` se o
|
|
151
|
+
padrão for claro.
|
|
152
|
+
|
|
153
|
+
## False Positives
|
|
154
|
+
|
|
155
|
+
* **CAS / conditional update fecha a janela** — `UPDATE … WHERE balance = :lido` faz B
|
|
156
|
+
falhar se A mudou. Confirmar o `WHERE` guarda a condição antes de reportar.
|
|
157
|
+
* **Unique constraint** — mesmo sem lock de aplicação, o banco rejeita a duplicata.
|
|
158
|
+
"Duplicate creation" é defendido se a constraint existe.
|
|
159
|
+
* **SELECT FOR UPDATE / serializable** — a transação isola; a janela não existe.
|
|
160
|
+
Confirmar o nível de isolamento real.
|
|
161
|
+
* **Incremento atômico** — `UPDATE count = count + 1` é atômico no DB; "counter
|
|
162
|
+
inflation" não aplica. Aplica só se o app lê e reescreve o valor calculado.
|
|
163
|
+
* **Estado não é compartilhado** — se cada request opera sobre sua própria linha
|
|
164
|
+
(isolada por chave), não há race no mesmo estado.
|
|
165
|
+
* **Cache com invalidação síncrona** — se a escrita invalida o cache antes de servir a
|
|
166
|
+
próxima leitura, a race cache-DB é defendida. Relacionado a `state-consistency-audit`.
|
|
167
|
+
* **Limite é soft** — se o limite é orientativo, "6/5" pode ser tolerado por design.
|
|
168
|
+
Marcar `POSSIBLE` e levantar como decisão de produto.
|
|
169
|
+
|
|
170
|
+
## Output Format
|
|
171
|
+
|
|
172
|
+
Para cada read-then-write sem atomicidade que viola um invariant sob concorrência, um
|
|
173
|
+
finding via `templates/audit-report.md`. Em **Reproduction**, dê a intercalação dos
|
|
174
|
+
dois requests (com timestamps/ordem) e o estado final observado. Em **Affected flow**,
|
|
175
|
+
nomeie o invariant (saldo ≥ 0, limite ≤ N, único). Em **Root cause**, diga o que falta
|
|
176
|
+
(transaction + FOR UPDATE, CAS, unique constraint, incremento atômico). Em
|
|
177
|
+
**Recommendation**, indique a defesa apropriada (preferir constraint/CAS no DB quando
|
|
178
|
+
possível — o banco é a última linha).
|
|
179
|
+
|
|
180
|
+
Apresente cada sequência como diagrama READ/DECISION/WRITE com a janela marcada.
|
|
181
|
+
Double-spend e over-limit primeiro; counter e cache-DB depois.
|