docs(21): capture phase context

This commit is contained in:
lorentz 2026-07-16 11:16:56 -04:00
parent f276dda754
commit 9ae3034f84
2 changed files with 330 additions and 0 deletions

View file

@ -0,0 +1,233 @@
# Phase 21: Autotask Triage Note - Context
**Gathered:** 2026-07-16
**Status:** Ready for planning
<domain>
## 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).
</domain>
<decisions>
## 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.
</decisions>
<canonical_refs>
## 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<T>`, ~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)
</canonical_refs>
<code_context>
## 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).
</code_context>
<specifics>
## 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.
</specifics>
<deferred>
## 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.
</deferred>
---
*Phase: 21-autotask-triage-note*
*Context gathered: 2026-07-16*

View file

@ -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"