---
phase: 05-verification-engine
plan: 02
subsystem: verification-consent
tags: [legal, consent, embed, cli, verify]
requirements: [VRFY-01, VRFY-06]
dependency-graph:
  requires:
    - "pkg/storage settings table GetSetting/SetSetting (Phase 1)"
    - "cmd/root.go cobra command tree (Phase 1)"
  provides:
    - "pkg/legal.Text() — compile-time embedded LEGAL.md"
    - "pkg/verify.EnsureConsent(db, in, out) — consent gate for --verify"
    - "keyhunter legal subcommand"
    - "verify.consent setting key contract"
  affects:
    - "cmd/root.go (new subcommand registration)"
tech-stack:
  added: []
  patterns:
    - "go:embed dual-location mirror (root LEGAL.md + pkg/legal/LEGAL.md), same pattern as providers/ vs pkg/providers/definitions/"
    - "Sticky-granted, transient-declined consent semantics (granted persists, declined re-prompts)"
    - "io.Reader/io.Writer injection for testable interactive prompts"
key-files:
  created:
    - "LEGAL.md"
    - "pkg/legal/LEGAL.md"
    - "pkg/legal/legal.go"
    - "pkg/legal/legal_test.go"
    - "cmd/legal.go"
    - "pkg/verify/consent.go"
    - "pkg/verify/consent_test.go"
    - ".planning/phases/05-verification-engine/05-02-SUMMARY.md"
  modified:
    - "cmd/root.go"
decisions:
  - "LEGAL.md duplicated at repo root and pkg/legal/ because go:embed cannot traverse parents"
  - "verify.consent setting: 'granted' is sticky, 'declined' is not — users who initially refuse can change their mind on the next run without manual reset"
  - "EnsureConsent takes io.Reader/io.Writer rather than reading os.Stdin/os.Stdout directly, so tests can drive it with strings.NewReader + bytes.Buffer"
  - "Consent prompt text explicitly mentions CFAA, Computer Misuse Act, GDPR, provider ToS and directs users to `keyhunter legal` for the full document"
metrics:
  duration: "~7m"
  completed_date: "2026-04-05"
  tasks_completed: 2
  files_created: 8
  files_modified: 1
---

# Phase 5 Plan 2: Legal Disclaimer & Consent Prompt Summary

**One-liner:** Shipped LEGAL.md embedded in the binary via go:embed, added a `keyhunter legal` subcommand, and implemented `verify.EnsureConsent` with sticky-granted / transient-declined semantics backed by the SQLite settings table.

## What Was Built

### LEGAL.md (repo root + pkg/legal mirror)
A 109-line user-facing disclaimer covering:
- **Purpose** — what the verification feature does at a high level
- **What --verify Does** — lightweight single HTTP call per key, no mutations, masked output
- **Legal Considerations** — CFAA (18 U.S.C. § 1030), UK Computer Misuse Act 1990, EU Directive 2013/40/EU, provider ToS
- **Responsible Use** — own keys, authorized engagements, own CI/CD only
- **Responsible Disclosure** — security.txt, never publish, reasonable rotation window
- **Disclaimer** — AS IS, no warranty, user bears sole responsibility
- **Consent Record** — explains the first-run prompt and where the decision is persisted

The root copy is what users read; `pkg/legal/LEGAL.md` is a byte-identical mirror required by `//go:embed` (Go embed directives cannot traverse `..`). This mirrors the Phase 1 decision to keep `providers/*.yaml` user-visible with a `pkg/providers/definitions/*.yaml` embed mirror.

### pkg/legal package
```go
//go:embed LEGAL.md
var legalMarkdown string
func Text() string { return legalMarkdown }
```
Two tests: `TestText_NonEmpty` (>500 bytes) and `TestText_ContainsKeyPhrases` (contains "CFAA", "Responsible Use", "Disclaimer"). Both pass.

### keyhunter legal command
`cmd/legal.go` registers a trivial `legalCmd` that prints `legal.Text()` to stdout. Wired in `cmd/root.go` alongside the existing `scanCmd`, `providersCmd`, `configCmd`. `go run . legal | grep CFAA` returns a match.

### pkg/verify.EnsureConsent
```go
func EnsureConsent(db *storage.DB, in io.Reader, out io.Writer) (bool, error)
```
Behavior:
1. Reads `verify.consent` from the settings table.
2. If previously `"granted"` → returns `(true, nil)` immediately, writes nothing.
3. Otherwise writes a multi-line warning to `out` referencing "legal implications", CFAA, Computer Misuse Act, GDPR, provider ToS, and pointing the user to `keyhunter legal`.
4. Reads one line from `in`, lowercases and trims.
5. If the answer is `"yes"` → persists `"granted"` and returns `(true, nil)`.
6. Any other input (including `"y"`, `"no"`, empty) → persists `"declined"` and returns `(false, nil)`.
7. Declined is **not** sticky — the next call re-prompts (found && value != "granted" falls through to the prompt branch).

Six test cases cover the full decision matrix:
- `TestEnsureConsent_GrantedPrevious` — sticky short-circuit with no prompt output
- `TestEnsureConsent_TypeYes` — prompts, persists granted, prompt text contains "legal implications" and "keyhunter legal"
- `TestEnsureConsent_TypeYesUppercase` — case-insensitive match
- `TestEnsureConsent_TypeNo` — persists declined, returns false
- `TestEnsureConsent_Empty` — empty input treated as decline
- `TestEnsureConsent_DeclinedNotSticky` — seed declined + type yes → re-prompted and now granted

All 6 pass.

## TDD Trace (Task 2)

- **RED** (`e5f7214`) — wrote `consent_test.go` referencing `ConsentSettingKey`, `ConsentGranted`, `ConsentDeclined`, and `EnsureConsent` before the implementation existed. Build failed with "undefined:" errors for all 5 symbols. ✅ confirmed failing.
- **GREEN** (`d4c1403`) — created `consent.go` with the constants and function. One iteration needed: the initial prompt wrapping split "legal implications" across two lines ("legal\nimplications"), which broke the literal substring assertion in `TestEnsureConsent_TypeYes`. Rewrapped the prompt text to keep the phrase contiguous on one line. All 6 tests green.
- **REFACTOR** — none needed; implementation matched the plan's reference code modulo the prompt rewrap.

## Verification

```
$ go build ./...
(clean)

$ go test ./pkg/legal/... ./pkg/verify/...
ok  github.com/salvacybersec/keyhunter/pkg/legal  0.002s
ok  github.com/salvacybersec/keyhunter/pkg/verify 0.331s

$ go run . legal | grep -c CFAA
1
```

Must-haves truths verified:
- ✅ `keyhunter legal` prints the embedded LEGAL.md to stdout (`grep -c CFAA` returns 1)
- ✅ First invocation prompts; "yes" (case-insensitive) proceeds; anything else aborts (6 tests)
- ✅ Consent decision stored in settings and not re-prompted when granted (`TestEnsureConsent_GrantedPrevious`)
- ✅ LEGAL.md ships via `go:embed` (`grep -q "go:embed LEGAL.md" pkg/legal/legal.go` — present)

## Commits

| Hash     | Type    | Summary |
|----------|---------|---------|
| 260e342  | feat    | Add LEGAL.md, pkg/legal embed, and `keyhunter legal` command |
| e5f7214  | test    | RED: failing consent tests |
| d4c1403  | feat    | GREEN: implement EnsureConsent |

## Deviations from Plan

None — plan executed as written. One minor in-task iteration: the prompt text in the reference code happened to wrap "legal implications" across two `Fprintln` calls, which broke the test assertion on literal substrings. Rewrapped the prompt lines to keep "legal implications" contiguous. Functionally identical; no new behavior.

## Out-of-Scope Note

During verification I observed an uncommitted modification to `pkg/verify/verifier.go` unrelated to this plan — it contains an in-progress `HTTPVerifier` implementation from what appears to be Plan 05-03 work (the git log shows `3ceccd9 test(05-03): add failing tests for HTTPVerifier single-key verification`). I deliberately did not touch that file; it will be committed by plan 05-03 executor.

## Self-Check: PASSED

All 8 claimed files exist on disk. All 3 claimed commits exist in git log.

## Dependencies Satisfied For Next Plans

- **05-03 (HTTP verifier)** — can call `verify.EnsureConsent` at the top of the scan/verify entry point once this plan lands
- **05-04 (verify CLI flag integration)** — has `ConsentSettingKey`, `ConsentGranted`, `ConsentDeclined` constants to key off
- **Any future plan** that needs to surface legal disclaimers or track user acknowledgements — can follow the same `pkg/legal` + dual-LEGAL.md embed pattern
