docs(21): capture phase context
This commit is contained in:
parent
f276dda754
commit
9ae3034f84
2 changed files with 330 additions and 0 deletions
233
.planning/phases/21-autotask-triage-note/21-CONTEXT.md
Normal file
233
.planning/phases/21-autotask-triage-note/21-CONTEXT.md
Normal 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*
|
||||
|
|
@ -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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue