feat: AI ticket analyzer (phases 1-6)
Multi-stage LLM pipeline that produces structured analyses of Autotask tickets from local Postgres. Migration 069 + Zod schemas, Stage 0 preprocessor, IT Glue redaction + search, Anthropic SDK wrapper, Stages 1/3/4 (Haiku/Sonnet/Opus), pipeline + cost circuit breaker, job worker (opt-in autostart), 6 API routes, 3 frontend pages, share-row persistence (email send deferred to phase 7). 128 vitest tests, tsc clean. Build journal in docs/wulf-pulse-ticket-analyzer-build-notes.md. Sync: adds syncTicketNotes() + ticket_notes to ordered/date-filtered entities so the analyzer's local mirror stays current via scheduler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea3471d38d
commit
8f8b5ab7be
53 changed files with 9377 additions and 33 deletions
64
lib/services/analyzer/itglue-redact.ts
Normal file
64
lib/services/analyzer/itglue-redact.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* IT Glue redaction.
|
||||
*
|
||||
* SECURITY-CRITICAL. This module recursively walks an arbitrary value
|
||||
* (object | array | scalar) and replaces any value whose KEY name looks like
|
||||
* a credential with the string "[REDACTED]". The original input is not
|
||||
* mutated — a deep-cloned copy is returned.
|
||||
*
|
||||
* Why it exists: IT Glue documents and configurations frequently embed plain-
|
||||
* text passwords, API keys, and tokens. None of those values may EVER reach
|
||||
* the LLM context, log lines, or any database row — only doc IDs and names.
|
||||
* See docs/wulf-pulse-ticket-analyzer-prompt.md → "Critical correctness notes".
|
||||
*
|
||||
* The match pattern is intentionally broad. False positives (a benign field
|
||||
* happens to be named "key") are acceptable; false negatives are not.
|
||||
*/
|
||||
|
||||
const SENSITIVE_KEY_PATTERN = /password|secret|key|token|credential|api[_-]?key/i;
|
||||
|
||||
export const REDACTED_VALUE = '[REDACTED]';
|
||||
|
||||
/**
|
||||
* Recursively redact values whose keys match the sensitive-key pattern.
|
||||
* Returns a new value; does not mutate the input.
|
||||
*/
|
||||
export function redact<T>(value: T): T {
|
||||
return redactInternal(value, new WeakSet()) as T;
|
||||
}
|
||||
|
||||
function redactInternal(value: unknown, seen: WeakSet<object>): unknown {
|
||||
if (value === null || value === undefined) return value;
|
||||
|
||||
// Primitives — return as-is.
|
||||
if (typeof value !== 'object') return value;
|
||||
|
||||
// Cycle guard — if we've already seen this object on this branch, return a
|
||||
// marker rather than recursing infinitely. (IT Glue payloads shouldn't have
|
||||
// cycles in practice, but defensive against malformed input.)
|
||||
if (seen.has(value as object)) return '[CIRCULAR]';
|
||||
seen.add(value as object);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => redactInternal(item, seen));
|
||||
}
|
||||
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (isSensitiveKey(key)) {
|
||||
// Even if the value is an object/array, replace the whole subtree —
|
||||
// a credentials block named "auth" should not leak its leaves.
|
||||
out[key] = nested === null || nested === undefined ? nested : REDACTED_VALUE;
|
||||
} else {
|
||||
out[key] = redactInternal(nested, seen);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exported for tests. True if the key name looks like a credential field.
|
||||
*/
|
||||
export function isSensitiveKey(key: string): boolean {
|
||||
return SENSITIVE_KEY_PATTERN.test(key);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue