diff --git a/.planning/phases/21-autotask-triage-note/21-CONTEXT.md b/.planning/phases/21-autotask-triage-note/21-CONTEXT.md new file mode 100644 index 0000000..ba85131 --- /dev/null +++ b/.planning/phases/21-autotask-triage-note/21-CONTEXT.md @@ -0,0 +1,233 @@ +# Phase 21: Autotask Triage Note - Context + +**Gathered:** 2026-07-16 +**Status:** Ready for planning + + +## Phase Boundary + +Once a campaign is classified (Phase 19) with whatever remediation state +currently exists (Phase 20 — proposed/approved/completed), produce a +human-readable, sanitized internal triage note summarizing verdict, evidence, +blast radius, and current remediation state. A safe Autotask note-write path +already exists in this codebase (`workflow-engine.ts`'s +`client.createEntity('TicketNotes', ...)`), so this phase writes the note to +every ticket linked to the campaign via a new on-demand endpoint — it does +NOT fall back to API-only return except when an individual write actually +fails at request time. Never a raw/unsanitized dump, never a silent no-op. + +Does NOT cover the LiveLink approval UI (Phase 22) — this phase is API/note- +generation only, no new UI (`UI hint: no` per ROADMAP.md). + + + + +## Implementation Decisions + +### Ticket Scoping +- **D-01:** The note is posted to **every ticket linked to the campaign** — + one `TicketNotes` write per `reports.ticket_id` in the campaign, not just + the newest or original report. A campaign can span multiple tickets + (`reports.campaign_id` is 1-to-many per `migrations/097_phishing_triage_ + schema.sql`); every duplicate-report ticket gets the same note content so + whoever's looking at any of them sees the full picture. + +### Trigger Point +- **D-02:** A new **on-demand, campaign-scoped endpoint** — e.g. + `POST /api/phishing/campaigns/{id}/triage-note` — matching the existing + `classify`/`approve`/`remediate`/`mark-false-positive` pattern (Phase 18 + D-06 route shape, Phase 19 D-02 on-demand-only precedent). It does **not** + fire automatically as a side effect of `POST /classify` — the operator + explicitly triggers it, consistent with Phase 19's decision to keep + classification (and by extension, every state-changing/reporting action in + this milestone) operator-initiated rather than an automatic cascade. + +### Re-trigger Behavior +- **D-03:** The endpoint is **idempotent-in-name-only** — it can be called + repeatedly for the same campaign (e.g. after new remediation approvals + change what the note should say), and **always posts a fresh note** + reflecting current state at call time. No dedupe/skip tracking against + prior triage-note calls. Duplicate Autotask notes across repeated calls are + expected and acceptable — same posture as Phase 19 D-02 allowing repeated + `/classify` calls to each append a new `classifications` row. + +### Note Content +- **D-04:** The note includes the **full picture**: classification verdict + + confidence + summary + reasons (Phase 19 `classifications` row), blast- + radius counts (Phase 17 `getBlastRadius()` — delivered/held/clicked/etc., + or an explicit "unavailable" if Mimecast isn't configured), AND the + **current** `remediation_actions` state for the campaign exactly as it + stands right now — `proposed` only if no operator has acted yet, or + `approved`/`completed` rows if they exist. The note reflects present truth, + not just the classifier's original recommendation frozen at classify-time. + This directly satisfies the phase's own success criteria wording + ("recommended actions") while staying accurate after approval/remediation + has happened. + +### Write-Failure Fallback +- **D-05:** If an individual ticket's `TicketNotes` write fails at request + time (Autotask API down, auth error, ticket deleted/closed, etc.), that + specific failure does **not** abort the whole call and does **not** get + silently swallowed. See D-06 for the exact response shape. This is the + ONLY case where "return note text instead of writing" applies — the NOTE-01 + "otherwise return via API" branch is about handling a runtime write + failure per-ticket, not a general capability gate (Pulse already has the + safe write path, confirmed by direct code inspection — see canonical refs). + +### Partial-Failure Response Shape +- **D-06:** The API response always includes: (a) the generated sanitized + note text itself (regardless of write outcome — never withheld), and (b) a + per-ticket status list (`ticket_id`, `posted: true/false`, error detail if + failed). No conceptual "rollback" is attempted or implied — a note already + posted to one ticket in the same call stays posted even if a sibling + ticket's write fails; the response just reports which succeeded so the + operator can manually paste the returned text into any ticket that failed. + +### Claude's Discretion (explicitly deferred to research + planner) +- Exact sanitization rules beyond "no raw secrets/tokens/full malicious URL + query strings" (NOTE-01) — e.g. whether URLs are truncated to + scheme+host+path with query strings stripped entirely, whether sender + email addresses/attachment hashes are shown as-is (they're evidence, not + secrets) — planner's call, following the same spirit as + `lib/services/analyzer/itglue-search.ts`'s existing redaction precedent + even though that's a different data source. +- Exact `noteType`/`publish` values to pass to `createEntity('TicketNotes', + ...)` — `workflow-engine.ts`'s existing call uses `noteType: 1` (commented + "Internal") and `publish: 1` ("All Autotask Users" — internal staff, not + customer portal); planner's call whether to reuse those exact values or + pick different ones, as long as the note is never customer/portal-visible. +- Exact note text format/template (markdown-ish sections vs. plain prose) — + no specific reference example surfaced; planner's call as long as it reads + as human-readable prose, not a JSON dump. +- Whether the triage-note endpoint requires the same elevated permission tier + as approve/remediate (`phishing:approve`) or the lower `phishing:analyze` + tier (same as classify) — no strong preference surfaced; planner's call, + leaning toward matching `classify`'s tier since generating a note is + informational, not a state-changing security decision like approve/ + remediate. +- Response envelope naming/shape details (camelCase per project convention) — + standard API convention applies, no new decision needed. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Safe Autotask write path (confirms NOTE-01's "if a safe path exists" premise — it does) +- `lib/services/workflow-engine.ts` (`runAiTroubleshooting`, ~line 560-613) — + existing precedent: `getAutotaskClient().createEntity('TicketNotes', { + ticketID, title, description, noteType: 1, publish: 1 })`. This IS the safe + write path NOTE-01 asks about — confirmed by direct inspection, not + assumed. +- `lib/services/autotask-client.ts` (`createEntity`, ~line 175) — generic + entity-create method used by the above; `lib/services/autotask-factory.ts` + → `getAutotaskClient()` for the singleton client. +- `lib/types/autotask.ts` (`TicketNote` interface, ~line 185) — + `{ id, ticketID, title?, description?, noteType?, publish?, + creatorResourceID?, creatorType?, lastActivityDate?, createDateTime? }`. +- `AUTOTASK_API_GUIDE.md` (~line 365) — `publish: 1 = All Autotask Users, + 2 = Internal Users Only` — both values keep the note off the customer + portal; neither is customer-visible. +- `.planning/codebase/INTEGRATIONS.md` (Outgoing Webhooks section) — + independent codebase-map confirmation: "Autotask write-back: workflow + engine executes Autotask API calls (POST notes, status updates, custom + fields)". + +### Schema (this phase reads, does not modify) +- `migrations/097_phishing_triage_schema.sql` — `campaigns` (id, status, + report_count), `reports` (ticket_id BIGINT NOT NULL REFERENCES tickets(id), + campaign_id nullable FK, UNIQUE(ticket_id) — the 1-to-many campaign→ticket + relationship D-01 is built on), `classifications` (verdict, confidence, + summary, reasons JSONB, recommended_actions JSONB, requires_approval), + `remediation_actions` (action_type, status, params, approved_by, + approved_at), `audit_events` (actor, event_type, payload). + +### Evidence inputs (read-only for this phase) +- `.planning/phases/19-classification-engine/19-CONTEXT.md` — classification + shape (verdict/confidence/summary/reasons/recommended_actions), D-08 action + vocabulary (7 action types, destructive vs non-destructive). +- `.planning/phases/17-mimecast-blast-radius-lookup/17-CONTEXT.md` — D-03, + `getBlastRadius()` is ephemeral (not persisted by Phase 17 itself); Phase + 19 persists what it needs into `classifications.reasons` — this phase may + need a fresh `getBlastRadius()` call or may read what Phase 19 already + captured, planner's call which is more current/accurate. +- `.planning/phases/20-remediation-approval-audit-safety/20-CONTEXT.md` — + `remediation_actions` status lifecycle (`proposed` → `approved` → + `completed`), D-01's simulated-effect model (no real external provider + wired yet in this milestone). + +### Auth/permissions (established by Phase 18, reused here) +- `lib/permissions.ts` — `phishing` resource statement; `analyze`/`approve`/ + `remediate` actions already granted per role (Phase 18 D-05, Phase 20 D-02). +- `lib/auth-utils.ts` — `requirePermission(resource, action)` — see + `app/api/phishing/campaigns/[id]/classify/route.ts` for the exact + call-and-early-return pattern this phase's new route should copy. + +### Prior phase decisions (for consistency) +- `.planning/phases/18-campaign-grouping-phishing-analysis-api/18-CONTEXT.md` + (D-06 — per-route `requirePermission` convention; campaign/report shape) + + + + +## Existing Code Insights + +### Reusable Assets +- `app/api/phishing/campaigns/[id]/classify/route.ts` — exact structural + precedent (UUID param guard, `requirePermission`, service delegation, + error shape) for the new triage-note route. +- `workflow-engine.ts`'s `createEntity('TicketNotes', {...})` call — copy + this shape directly rather than reinventing the Autotask write. + +### Established Patterns +- `requirePermission(resource, action)` early-return pattern for every + `/api/phishing/*` route (Phase 18 D-06). +- Campaign-scoped routes read the campaign's linked `reports` to get + `ticket_id`s (same join `campaign-grouping-service.ts` and the classifier + already do to gather evidence per campaign). + +### Integration Points +- New route reads: `campaigns` row, all linked `reports` (for ticket_ids), + most recent `classifications` row, most recent/relevant + `remediation_actions` rows, and blast-radius data. +- New route writes: one `TicketNotes` entity per linked ticket via + `getAutotaskClient().createEntity()`. Does NOT write to any + `phishing_triage` schema table itself — this phase is a read+external-write + phase, no new Postgres writes needed (no note-history table decided here; + see Claude's Discretion if planner finds a reason to persist sent notes). + + + + +## Specific Ideas + +No specific note-text template/example was provided — the process was to +identify the concrete data model gap (does a safe write path exist? yes, +confirmed by inspection) and lock down operational decisions (ticket +scoping, re-trigger semantics, partial-failure handling) rather than +literal wording. + + + + +## Deferred Ideas + +- **Persisting sent-note history** (a table tracking which notes were posted + to which tickets, when) — not requested, no signal it's needed; planner + should treat as out of scope unless a success criterion requires proving + a note was sent (Autotask itself is the record of truth for what got + posted). +- **Dedup/skip-if-unchanged logic** — explicitly rejected in favor of D-03 + (always post fresh); noted here so a future phase doesn't reintroduce it + without reason. + +None — discussion stayed within phase scope. + + + +--- + +*Phase: 21-autotask-triage-note* +*Context gathered: 2026-07-16* diff --git a/.planning/phases/21-autotask-triage-note/21-DISCUSSION-LOG.md b/.planning/phases/21-autotask-triage-note/21-DISCUSSION-LOG.md new file mode 100644 index 0000000..7fd080f --- /dev/null +++ b/.planning/phases/21-autotask-triage-note/21-DISCUSSION-LOG.md @@ -0,0 +1,97 @@ +# Phase 21: Autotask Triage Note - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-07-16 +**Phase:** 21-autotask-triage-note +**Areas discussed:** Ticket scoping, Trigger point, Re-trigger behavior, Note content, Write-failure fallback, Partial-failure response shape + +--- + +## Ticket Scoping + +| Option | Description | Selected | +|--------|-------------|----------| +| All linked tickets | Post the same note to every report's ticket_id in the campaign | ✓ | +| Most recent report's ticket | Post only to the most-recently-reported duplicate's ticket | | +| Original (first) report's ticket | Post only to the earliest report's ticket | | +| Operator specifies the ticket | Endpoint takes a ticket_id param, operator controls target | | + +**User's choice:** All linked tickets +**Notes:** A campaign groups multiple duplicate-report tickets (`campaigns`→`reports` is 1-to-many). Every duplicate-report ticket gets the same note so whoever looks at any of them sees the full picture, at the cost of N Autotask writes per trigger. + +--- + +## Trigger Point + +| Option | Description | Selected | +|--------|-------------|----------| +| New on-demand endpoint | Operator explicitly triggers, matches classify/approve/remediate pattern | ✓ | +| Auto-fire after classify | Fires automatically as a side effect of a successful /classify call | | + +**User's choice:** New on-demand endpoint +**Notes:** Matches Phase 19 D-02's precedent of keeping classification (and by extension every state-changing/reporting action this milestone) operator-initiated, not an automatic cascade. + +--- + +## Re-trigger Behavior (follow-up on Ticket Scoping) + +| Option | Description | Selected | +|--------|-------------|----------| +| Always post fresh | Every call posts a new note reflecting current state to every linked ticket | ✓ | +| Skip tickets that already have a note from this exact classification | Track (campaign, classification_id, ticket_id) combos and skip duplicates | | + +**User's choice:** Always post fresh +**Notes:** Duplicate Autotask notes across repeated calls are acceptable — same posture as Phase 19 D-02 allowing repeated `/classify` calls. + +--- + +## Note Content + +| Option | Description | Selected | +|--------|-------------|----------| +| Full picture: verdict + evidence + blast radius + current remediation state | Note reflects present truth, not just the classifier's frozen recommendation | ✓ | +| Classification + blast radius only, no remediation section | Simpler, but conflicts with the phase's own success criteria wording | | + +**User's choice:** Full picture +**Notes:** Directly satisfies the phase's success criteria ("recommended actions") while staying accurate after approval/remediation has actually happened. + +--- + +## Write-Failure Fallback + +| Option | Description | Selected | +|--------|-------------|----------| +| Fall back to returning note text via API response | Matches "never a silent no-op" — operator gets text to paste manually | ✓ | +| Return an explicit error, no fallback | Simpler, but operator gets nothing usable back for that request | | + +**User's choice:** Fall back to returning note text via API response +**Notes:** This is the actual scope of NOTE-01's "otherwise return via API" branch — a per-ticket runtime write failure, not a general capability gate (the safe write path already exists, confirmed by direct code inspection of `workflow-engine.ts`). + +--- + +## Partial Failure (follow-up on multi-ticket writes) + +| Option | Description | Selected | +|--------|-------------|----------| +| Per-ticket status + note text always included | Response lists each ticket's outcome and always includes the note text | ✓ | +| All-or-nothing: any failure fails the whole request | Simpler semantics, but can't actually un-post already-successful writes | | + +**User's choice:** Per-ticket status + note text always included +**Notes:** A conceptual "rollback" isn't real here — a note already posted to one ticket stays posted even if a sibling ticket's write fails in the same call. + +--- + +## Claude's Discretion + +- Exact sanitization rules beyond "no raw secrets/tokens/full malicious URL query strings" +- Exact `noteType`/`publish` values passed to `createEntity('TicketNotes', ...)` +- Exact note text format/template (prose structure) +- Whether the endpoint requires `phishing:approve` or `phishing:analyze` permission tier +- Response envelope naming details (camelCase convention applies) + +## Deferred Ideas + +- Persisting sent-note history (a table tracking which notes were posted, when) — not requested, Autotask itself is the record of truth +- Dedup/skip-if-unchanged logic — explicitly rejected in favor of "always post fresh"