- Add DispatchContext field to Task model for rule metadata storage
- Add BlocklistEmails config (BLOCKLIST_EMAILS env var)
- Wire rules.Pipeline into IMAPWatcher, replacing inline whitelist check
- Pass rule metadata through DispatchContext to OpenClaw dispatch
- Remove isWhitelisted method (now handled by WhitelistRule)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[High] internal/mail/imap.go:359 still evaluates external rules before the duplicate guard at internal/mail/imap.go:372. The design this PR introduces says idempotency is an internal safety check that must run before external rules (docs/adr/0002-external-rules-pipeline.md:17). As written, duplicate messages can still be re-evaluated by policy and create extra IGNORED rows before the idempotency check runs, so the ingress order is still wrong.
[Medium] requirements.md:66, requirements.md:143, and requirements.md:162 still describe the old flow and config: ingress step 2 is hardcoded whitelist, only WHITELIST_EMAILS is documented, and the acceptance/test matrix does not cover the new external-rules/context stage. Updating requirements.md was part of the locked issue scope, so the main spec is still out of sync with the code, README, and ADR.
Validation:
gofmt -l .: OK
go vet ./...: OK
go test ./...: OK
FAIL
1. [High] `internal/mail/imap.go:359` still evaluates external rules before the duplicate guard at `internal/mail/imap.go:372`. The design this PR introduces says idempotency is an internal safety check that must run before external rules (`docs/adr/0002-external-rules-pipeline.md:17`). As written, duplicate messages can still be re-evaluated by policy and create extra `IGNORED` rows before the idempotency check runs, so the ingress order is still wrong.
2. [Medium] `requirements.md:66`, `requirements.md:143`, and `requirements.md:162` still describe the old flow and config: ingress step 2 is hardcoded whitelist, only `WHITELIST_EMAILS` is documented, and the acceptance/test matrix does not cover the new external-rules/context stage. Updating `requirements.md` was part of the locked issue scope, so the main spec is still out of sync with the code, README, and ADR.
Validation:
- gofmt -l .: OK
- go vet ./...: OK
- go test ./...: OK
- Move idempotency before external rules per ADR-0002
- Update requirements.md: new pipeline flow, BLOCKLIST_EMAILS,
dispatch_context metadata
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[High] internal/ai_client/client.go:202 initializes metadata.task_uuid, then internal/ai_client/client.go:205-208 blindly overlays every key from DispatchContext. That lets rule-derived metadata overwrite the callback correlation key. The callback handler in internal/api/router.go:15-17 and internal/api/router.go:69-83 trusts metadata.task_uuid to find the task, so a reserved-key collision would break callback routing for the new context feature this PR introduces.
[Medium] requirements.md:166-180 is still only partially updated. The acceptance criteria and test matrix still describe the old whitelist-only behavior and do not cover the new external-rules stage, blocklist support, or dispatch_context propagation. The locked scope for issue #5 included keeping the specs aligned with the new pipeline, so requirements.md is still stale in that section.
Validation:
gofmt -l .: OK
go vet ./...: OK
go test ./...: OK
FAIL
1. [High] `internal/ai_client/client.go:202` initializes `metadata.task_uuid`, then `internal/ai_client/client.go:205-208` blindly overlays every key from `DispatchContext`. That lets rule-derived metadata overwrite the callback correlation key. The callback handler in `internal/api/router.go:15-17` and `internal/api/router.go:69-83` trusts `metadata.task_uuid` to find the task, so a reserved-key collision would break callback routing for the new context feature this PR introduces.
2. [Medium] `requirements.md:166-180` is still only partially updated. The acceptance criteria and test matrix still describe the old whitelist-only behavior and do not cover the new external-rules stage, blocklist support, or `dispatch_context` propagation. The locked scope for issue `#5` included keeping the specs aligned with the new pipeline, so `requirements.md` is still stale in that section.
Validation:
- gofmt -l .: OK
- go vet ./...: OK
- go test ./...: OK
- Skip task_uuid key when merging DispatchContext into dispatch metadata
- Update requirements.md acceptance criteria and test matrix for external rules
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. [High] task_uuid protected — buildDispatchMetadata now skips the task_uuid key when merging DispatchContext, preventing rule metadata from overwriting the callback correlation key.
2. [Medium] requirements.md acceptance criteria updated — Test matrix now covers:
Anti-loop + idempotency + external rules order (item 6)
Whitelist rejection via external rules (item 7)
Blocklist rejection via external rules (item 8)
Rule metadata propagation to OpenClaw (item 9)
Validation:
gofmt -l . — clean
go vet ./... — OK
go test ./... — all PASS
## Review feedback addressed (round 2)
**1. [High] `task_uuid` protected** — `buildDispatchMetadata` now skips the `task_uuid` key when merging DispatchContext, preventing rule metadata from overwriting the callback correlation key.
**2. [Medium] requirements.md acceptance criteria updated** — Test matrix now covers:
- Anti-loop + idempotency + external rules order (item 6)
- Whitelist rejection via external rules (item 7)
- Blocklist rejection via external rules (item 8)
- Rule metadata propagation to OpenClaw (item 9)
Validation:
- `gofmt -l .` — clean
- `go vet ./...` — OK
- `go test ./...` — all PASS
[Medium] requirements.md:38-53 still omits the new dispatch_context column, even though this PR persists it on internal/database/models.go:19-28 and relies on it later in the flow at requirements.md:72-87. The requirements doc is still internally inconsistent about the task schema.
[Medium] internal/ai_client/client.go:201-213 still has no direct test coverage. internal/ai_client/client_test.go:117-156 verifies callback_url, but it never asserts that DispatchContext metadata is forwarded or that task_uuid cannot be overwritten. That merge path already needed a bugfix in the previous review round, so leaving it untested keeps regression risk high.
Validation:
gofmt -l .: OK
go vet ./...: OK
go test ./...: OK
FAIL
1. [Medium] `requirements.md:38-53` still omits the new `dispatch_context` column, even though this PR persists it on `internal/database/models.go:19-28` and relies on it later in the flow at `requirements.md:72-87`. The requirements doc is still internally inconsistent about the task schema.
2. [Medium] `internal/ai_client/client.go:201-213` still has no direct test coverage. `internal/ai_client/client_test.go:117-156` verifies `callback_url`, but it never asserts that `DispatchContext` metadata is forwarded or that `task_uuid` cannot be overwritten. That merge path already needed a bugfix in the previous review round, so leaving it untested keeps regression risk high.
Validation:
- gofmt -l .: OK
- go vet ./...: OK
- go test ./...: OK
1. requirements.md — added dispatch_context to task schema
The dispatch_context column was missing from the schema section (§3). Added between attempt_smtp and last_error:
- `dispatch_context` TEXT (JSON metadata từ external rules pipeline, truyền qua OpenClaw dispatch)
2. buildDispatchMetadata — direct test coverage
Added 3 new tests in client_test.go:
Test
Validates
TestBuildDispatchMetadata_ForwardsContext
Rule metadata (rule, source) is forwarded into dispatch payload
TestBuildDispatchMetadata_ProtectsTaskUUID
task_uuid in DispatchContext cannot overwrite the real task UUID
TestBuildDispatchMetadata_NoContext
Empty DispatchContext produces only task_uuid key
Exported via ExportBuildDispatchMetadata (thin wrapper) to keep the private function untouched.
=== RUN TestBuildDispatchMetadata_ForwardsContext --- PASS
=== RUN TestBuildDispatchMetadata_ProtectsTaskUUID --- PASS
=== RUN TestBuildDispatchMetadata_NoContext --- PASS
Full suite green: go test ./... — all packages PASS.
## Round 4 fixes (commit `c127018`)
### 1. `requirements.md` — added `dispatch_context` to task schema
The `dispatch_context` column was missing from the schema section (`§3`). Added between `attempt_smtp` and `last_error`:
```
- `dispatch_context` TEXT (JSON metadata từ external rules pipeline, truyền qua OpenClaw dispatch)
```
### 2. `buildDispatchMetadata` — direct test coverage
Added 3 new tests in `client_test.go`:
| Test | Validates |
|---|---|
| `TestBuildDispatchMetadata_ForwardsContext` | Rule metadata (`rule`, `source`) is forwarded into dispatch payload |
| `TestBuildDispatchMetadata_ProtectsTaskUUID` | `task_uuid` in DispatchContext cannot overwrite the real task UUID |
| `TestBuildDispatchMetadata_NoContext` | Empty DispatchContext produces only `task_uuid` key |
Exported via `ExportBuildDispatchMetadata` (thin wrapper) to keep the private function untouched.
```
=== RUN TestBuildDispatchMetadata_ForwardsContext --- PASS
=== RUN TestBuildDispatchMetadata_ProtectsTaskUUID --- PASS
=== RUN TestBuildDispatchMetadata_NoContext --- PASS
```
Full suite green: `go test ./...` — all packages PASS.
[Medium] internal/ai_client/client_test.go:15 is not gofmt-formatted. gofmt -l . reports that file, so this revision does not yet satisfy the repo validation bar or the PR’s own stated test plan (gofmt -l . clean).
FAIL
1. [Medium] `internal/ai_client/client_test.go:15` is not `gofmt`-formatted. `gofmt -l .` reports that file, so this revision does not yet satisfy the repo validation bar or the PR’s own stated test plan (`gofmt -l .` clean).
Validation:
- gofmt -l .: FAIL (`internal/ai_client/client_test.go`)
- go vet ./...: OK
- go test ./...: OK
client_test.go was not gofmt-formatted. Fixed with gofmt -w.
gofmt -l . → clean
go vet ./... → OK
go test ./... → all PASS
## Round 5 fix (commit `094bd35`)
`client_test.go` was not `gofmt`-formatted. Fixed with `gofmt -w`.
```
gofmt -l . → clean
go vet ./... → OK
go test ./... → all PASS
```
codex
approved these changes 2026-04-28 07:14:00 +07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
internal/rules/package withRuleinterface,Pipeline,WhitelistRule, andBlocklistRule.DispatchContexton Task and pass to OpenClaw dispatch.BLOCKLIST_EMAILSenv var (complements existingWHITELIST_EMAILS).Changes
internal/rules/— types, pipeline, whitelist rule, blocklist rule + tests (21 tests)internal/mail/imap.go— pipeline replaces hardcoded whitelist,isWhitelistedremovedinternal/database/models.go—DispatchContextfield on Taskinternal/config/config.go—BLOCKLIST_EMAILSparsinginternal/ai_client/client.go— merge dispatch context into OpenClaw metadataREADME.md— updated flow diagramdocs/adr/0002-external-rules-pipeline.md.env.example— addedBLOCKLIST_EMAILSTest Plan
go test ./internal/rules/... -v— 17 tests PASSgo test ./internal/mail/... -v— 12 tests PASSgo test ./...— all packages PASSgo build ./...— OKgo vet ./...— OKgofmt -l .— cleanFixes #5
FAIL
internal/mail/imap.go:359still evaluates external rules before the duplicate guard atinternal/mail/imap.go:372. The design this PR introduces says idempotency is an internal safety check that must run before external rules (docs/adr/0002-external-rules-pipeline.md:17). As written, duplicate messages can still be re-evaluated by policy and create extraIGNOREDrows before the idempotency check runs, so the ingress order is still wrong.requirements.md:66,requirements.md:143, andrequirements.md:162still describe the old flow and config: ingress step 2 is hardcoded whitelist, onlyWHITELIST_EMAILSis documented, and the acceptance/test matrix does not cover the new external-rules/context stage. Updatingrequirements.mdwas part of the locked issue scope, so the main spec is still out of sync with the code, README, and ADR.Validation:
Review feedback addressed
1. [High] Ingress order fixed — Idempotency now runs before external rules:
Matches ADR-0002. Duplicates no longer trigger rule evaluation or create IGNORED rows.
2. [Medium] requirements.md updated — Ingress flow now shows 5 steps with external rules pipeline, added
BLOCKLIST_EMAILSconfig, addeddispatch_contextto dispatch metadata.Validation:
gofmt -l .— cleango vet ./...— OKgo test ./...— all PASSFAIL
internal/ai_client/client.go:202initializesmetadata.task_uuid, theninternal/ai_client/client.go:205-208blindly overlays every key fromDispatchContext. That lets rule-derived metadata overwrite the callback correlation key. The callback handler ininternal/api/router.go:15-17andinternal/api/router.go:69-83trustsmetadata.task_uuidto find the task, so a reserved-key collision would break callback routing for the new context feature this PR introduces.requirements.md:166-180is still only partially updated. The acceptance criteria and test matrix still describe the old whitelist-only behavior and do not cover the new external-rules stage, blocklist support, ordispatch_contextpropagation. The locked scope for issue#5included keeping the specs aligned with the new pipeline, sorequirements.mdis still stale in that section.Validation:
Review feedback addressed (round 2)
1. [High]
task_uuidprotected —buildDispatchMetadatanow skips thetask_uuidkey when merging DispatchContext, preventing rule metadata from overwriting the callback correlation key.2. [Medium] requirements.md acceptance criteria updated — Test matrix now covers:
Validation:
gofmt -l .— cleango vet ./...— OKgo test ./...— all PASSFAIL
requirements.md:38-53still omits the newdispatch_contextcolumn, even though this PR persists it oninternal/database/models.go:19-28and relies on it later in the flow atrequirements.md:72-87. The requirements doc is still internally inconsistent about the task schema.internal/ai_client/client.go:201-213still has no direct test coverage.internal/ai_client/client_test.go:117-156verifiescallback_url, but it never asserts thatDispatchContextmetadata is forwarded or thattask_uuidcannot be overwritten. That merge path already needed a bugfix in the previous review round, so leaving it untested keeps regression risk high.Validation:
Round 4 fixes (commit
c127018)1.
requirements.md— addeddispatch_contextto task schemaThe
dispatch_contextcolumn was missing from the schema section (§3). Added betweenattempt_smtpandlast_error:2.
buildDispatchMetadata— direct test coverageAdded 3 new tests in
client_test.go:TestBuildDispatchMetadata_ForwardsContextrule,source) is forwarded into dispatch payloadTestBuildDispatchMetadata_ProtectsTaskUUIDtask_uuidin DispatchContext cannot overwrite the real task UUIDTestBuildDispatchMetadata_NoContexttask_uuidkeyExported via
ExportBuildDispatchMetadata(thin wrapper) to keep the private function untouched.Full suite green:
go test ./...— all packages PASS.FAIL
internal/ai_client/client_test.go:15is notgofmt-formatted.gofmt -l .reports that file, so this revision does not yet satisfy the repo validation bar or the PR’s own stated test plan (gofmt -l .clean).Validation:
internal/ai_client/client_test.go)Round 5 fix (commit
094bd35)client_test.gowas notgofmt-formatted. Fixed withgofmt -w.PASS
No blocking issues found.
Validation: