@shield-agent/kya 0.33.6 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/.mcp.json +1 -1
  3. package/README.md +22 -5
  4. package/amp/settings.example.json +1 -1
  5. package/assets/screenshot-features.json +17 -0
  6. package/claude/claude_desktop_config.example.json +1 -1
  7. package/cline/cline_mcp_settings.example.json +1 -1
  8. package/copilot/mcp-config.example.json +1 -1
  9. package/dist/alerts/config.d.ts +22 -0
  10. package/dist/alerts/config.d.ts.map +1 -0
  11. package/dist/alerts/config.js +59 -0
  12. package/dist/alerts/config.js.map +1 -0
  13. package/dist/alerts/constants.d.ts +5 -0
  14. package/dist/alerts/constants.d.ts.map +1 -0
  15. package/dist/alerts/constants.js +5 -0
  16. package/dist/alerts/constants.js.map +1 -0
  17. package/dist/alerts/desktop.d.ts +19 -0
  18. package/dist/alerts/desktop.d.ts.map +1 -0
  19. package/dist/alerts/desktop.js +57 -0
  20. package/dist/alerts/desktop.js.map +1 -0
  21. package/dist/alerts/incident-alert.d.ts +42 -0
  22. package/dist/alerts/incident-alert.d.ts.map +1 -0
  23. package/dist/alerts/incident-alert.js +84 -0
  24. package/dist/alerts/incident-alert.js.map +1 -0
  25. package/dist/alerts/scan.d.ts +37 -0
  26. package/dist/alerts/scan.d.ts.map +1 -0
  27. package/dist/alerts/scan.js +188 -0
  28. package/dist/alerts/scan.js.map +1 -0
  29. package/dist/alerts/state.d.ts +55 -0
  30. package/dist/alerts/state.d.ts.map +1 -0
  31. package/dist/alerts/state.js +178 -0
  32. package/dist/alerts/state.js.map +1 -0
  33. package/dist/alerts/triage-input.d.ts +36 -0
  34. package/dist/alerts/triage-input.d.ts.map +1 -0
  35. package/dist/alerts/triage-input.js +159 -0
  36. package/dist/alerts/triage-input.js.map +1 -0
  37. package/dist/alerts/triage-model.d.ts +42 -0
  38. package/dist/alerts/triage-model.d.ts.map +1 -0
  39. package/dist/alerts/triage-model.js +126 -0
  40. package/dist/alerts/triage-model.js.map +1 -0
  41. package/dist/alerts/triage-prompt.d.ts +23 -0
  42. package/dist/alerts/triage-prompt.d.ts.map +1 -0
  43. package/dist/alerts/triage-prompt.js +53 -0
  44. package/dist/alerts/triage-prompt.js.map +1 -0
  45. package/dist/alerts/triage-schema.json +52 -0
  46. package/dist/alerts/triage-setup.d.ts +73 -0
  47. package/dist/alerts/triage-setup.d.ts.map +1 -0
  48. package/dist/alerts/triage-setup.js +280 -0
  49. package/dist/alerts/triage-setup.js.map +1 -0
  50. package/dist/alerts/triage-status.d.ts +26 -0
  51. package/dist/alerts/triage-status.d.ts.map +1 -0
  52. package/dist/alerts/triage-status.js +61 -0
  53. package/dist/alerts/triage-status.js.map +1 -0
  54. package/dist/alerts/triage-tokenizer.d.ts +29 -0
  55. package/dist/alerts/triage-tokenizer.d.ts.map +1 -0
  56. package/dist/alerts/triage-tokenizer.js +82 -0
  57. package/dist/alerts/triage-tokenizer.js.map +1 -0
  58. package/dist/cli.d.ts.map +1 -1
  59. package/dist/cli.js +14 -0
  60. package/dist/cli.js.map +1 -1
  61. package/dist/commands/alerts.d.ts +16 -0
  62. package/dist/commands/alerts.d.ts.map +1 -0
  63. package/dist/commands/alerts.js +112 -0
  64. package/dist/commands/alerts.js.map +1 -0
  65. package/dist/commands/notify-flush.d.ts.map +1 -1
  66. package/dist/commands/notify-flush.js +3 -0
  67. package/dist/commands/notify-flush.js.map +1 -1
  68. package/dist/commands/receipt-serve.d.ts +1 -1
  69. package/dist/commands/receipt-serve.d.ts.map +1 -1
  70. package/dist/commands/receipt-serve.js +3 -2
  71. package/dist/commands/receipt-serve.js.map +1 -1
  72. package/dist/investigate/detectors.d.ts +6 -0
  73. package/dist/investigate/detectors.d.ts.map +1 -1
  74. package/dist/investigate/detectors.js +10 -0
  75. package/dist/investigate/detectors.js.map +1 -1
  76. package/dist/notify/dispatch.d.ts +1 -1
  77. package/dist/notify/dispatch.d.ts.map +1 -1
  78. package/dist/notify/dispatch.js +4 -1
  79. package/dist/notify/dispatch.js.map +1 -1
  80. package/dist/notify/incident-templates.d.ts +23 -0
  81. package/dist/notify/incident-templates.d.ts.map +1 -0
  82. package/dist/notify/incident-templates.js +128 -0
  83. package/dist/notify/incident-templates.js.map +1 -0
  84. package/dist/notify/index.d.ts +12 -0
  85. package/dist/notify/index.d.ts.map +1 -1
  86. package/dist/notify/index.js +50 -3
  87. package/dist/notify/index.js.map +1 -1
  88. package/dist/notify/templates.d.ts +6 -0
  89. package/dist/notify/templates.d.ts.map +1 -1
  90. package/dist/notify/templates.js +12 -2
  91. package/dist/notify/templates.js.map +1 -1
  92. package/dist/receipt/live-server.d.ts +4 -0
  93. package/dist/receipt/live-server.d.ts.map +1 -1
  94. package/dist/receipt/live-server.js +48 -0
  95. package/dist/receipt/live-server.js.map +1 -1
  96. package/dist/receipt/render-receipt.d.ts +3 -0
  97. package/dist/receipt/render-receipt.d.ts.map +1 -1
  98. package/dist/receipt/render-receipt.js +28 -1
  99. package/dist/receipt/render-receipt.js.map +1 -1
  100. package/dist/trail.d.ts +6 -0
  101. package/dist/trail.d.ts.map +1 -1
  102. package/dist/trail.js +11 -0
  103. package/dist/trail.js.map +1 -1
  104. package/droid/README.md +1 -1
  105. package/droid/mcp.example.json +1 -1
  106. package/gemini/settings.example.json +1 -1
  107. package/grok/README.md +2 -2
  108. package/kilo/kilo.example.json +1 -1
  109. package/kimi/mcp.example.json +1 -1
  110. package/kiro/mcp.example.json +1 -1
  111. package/manifest.json +1 -1
  112. package/mastracode/mcp.example.json +1 -1
  113. package/mcp.json +1 -1
  114. package/openai/codex.config.example.toml +1 -1
  115. package/opencode/opencode.example.json +1 -1
  116. package/package.json +1 -1
  117. package/qwen/settings.example.json +1 -1
  118. package/server.json +2 -2
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Incident alert scan. Runs the deterministic detectors over the trail events of the last
3
+ * `windowMinutes`, keeps incidents at or above the severity floor, drops the ones already alerted
4
+ * (alerts-state.json) and sends `incident.opened` to the user-level webhooks and the desktop.
5
+ * Optional local triage model: when installed, each new alert gets category, urgency, action and benign
6
+ * (advisory text only). The model never decides whether an alert is sent: no model, a failed model and a
7
+ * slow model all give the same alert with the detector severity.
8
+ *
9
+ * Callers: the detached `kya notify-flush` child and the report daemon. Both are off the hook path.
10
+ * Advisory and fail open: nothing here throws, and a failed scan changes no verdict.
11
+ * Receivers and settings come from the user-level config only; a project config is never read.
12
+ */
13
+ import { kyaHome, readFileConfig } from "../config.js";
14
+ import { investigateTrail } from "../investigate/index.js";
15
+ import { notifyIncident, resolveIncidentWebhooks } from "../notify/index.js";
16
+ import { readGlobalTrail } from "../trail.js";
17
+ import { resolveAlertsConfig, severityRank } from "./config.js";
18
+ import { desktopNotify } from "./desktop.js";
19
+ import { buildIncidentAlert } from "./incident-alert.js";
20
+ import { incidentRef, needsAlert, readAlertsState, withStateLock, writeAlertsState, } from "./state.js";
21
+ import { buildTriageInput } from "./triage-input.js";
22
+ import { triageInstalled } from "./triage-status.js";
23
+ const NONE = { alerted: [], delivered: 0 };
24
+ /** Model load plus one inference. Longer than this and the alert goes out without triage. */
25
+ export const TRIAGE_TIMEOUT_MS = 8_000;
26
+ /** One scan opens at most this many desktop notifications (an incident storm must not flood the screen). */
27
+ export const MAX_DESKTOP_PER_SCAN = 3;
28
+ function withTimeout(p, ms) {
29
+ let timer;
30
+ const t = new Promise((resolve) => {
31
+ timer = setTimeout(() => resolve(undefined), ms);
32
+ timer.unref?.();
33
+ });
34
+ return Promise.race([p, t]).finally(() => clearTimeout(timer));
35
+ }
36
+ /** The model is loaded here, in the flush child or the report daemon, and only when an alert is new. Never in the hook. */
37
+ function defaultTriage(env) {
38
+ if (!triageInstalled(env))
39
+ return undefined;
40
+ return async (input) => (await import("./triage-model.js")).triageIncident(input, env);
41
+ }
42
+ /**
43
+ * Add triage to each claimed alert on its first attempt. A retry never loads the model again: a crash of the
44
+ * model code (it is native code) must not be able to eat the retries of an alert. Any failure leaves the alert as it is.
45
+ */
46
+ async function addTriage(due, events, env, deps) {
47
+ const run = deps.triage ?? defaultTriage(env);
48
+ const todo = due.filter((d) => d.first);
49
+ if (!run || todo.length === 0)
50
+ return;
51
+ try {
52
+ let slow = false;
53
+ for (const d of todo) {
54
+ if (slow)
55
+ break; // no result (timeout or a failing model): the rest of this scan goes out without triage, at once
56
+ try {
57
+ const triage = await withTimeout(run(buildTriageInput(d.inc, events)), deps.triageTimeoutMs ?? TRIAGE_TIMEOUT_MS);
58
+ if (triage)
59
+ d.alert = { ...d.alert, triage };
60
+ else
61
+ slow = true;
62
+ }
63
+ catch {
64
+ /* fail open: no triage, same alert */
65
+ }
66
+ }
67
+ }
68
+ finally {
69
+ // The report daemon lives long: give the model memory back after each scan.
70
+ if (!deps.triage)
71
+ await (await import("./triage-model.js")).releaseTriageModel().catch(() => undefined);
72
+ }
73
+ }
74
+ /** Events may be at most this far in the future (clock skew). Later ones are forged or broken. */
75
+ export const MAX_FUTURE_MS = 5 * 60_000;
76
+ /** Events inside the window. A ts that does not parse, or lies more than 5 minutes ahead, is dropped. */
77
+ export function windowEvents(events, now, windowMinutes) {
78
+ const from = now.getTime() - windowMinutes * 60_000;
79
+ const to = now.getTime() + MAX_FUTURE_MS;
80
+ return events.filter((e) => {
81
+ const t = Date.parse(e.ts);
82
+ return !Number.isNaN(t) && t >= from && t <= to;
83
+ });
84
+ }
85
+ /** Pure core: events in, incidents at or above the floor out. */
86
+ export function incidentsAboveFloor(events, minSeverity) {
87
+ const result = investigateTrail(events);
88
+ return {
89
+ incidents: result.incidents.filter((i) => severityRank(i.severity) >= severityRank(minSeverity)),
90
+ briefs: result.briefs,
91
+ };
92
+ }
93
+ async function saveTriage(env, claimed) {
94
+ await withStateLock(env, () => {
95
+ const byKey = new Map(claimed.map((c) => [c.key, c.alert.triage]));
96
+ writeAlertsState(env, readAlertsState(env).map((e) => {
97
+ const triage = byKey.get(e.key);
98
+ return triage ? { ...e, triage } : e;
99
+ }));
100
+ });
101
+ }
102
+ export async function runAlertScan(_cwd, env, deps = {}) {
103
+ try {
104
+ const cfg = resolveAlertsConfig(env);
105
+ if (!cfg.enabled)
106
+ return NONE;
107
+ const now = (deps.now ?? (() => new Date()))();
108
+ // User-level trail only (tail-capped): a project trail ships with a cloned repo and could be forged.
109
+ const events = windowEvents(readGlobalTrail(env), now, cfg.windowMinutes);
110
+ if (events.length === 0)
111
+ return NONE;
112
+ const { incidents, briefs } = incidentsAboveFloor(events, cfg.minSeverity);
113
+ if (incidents.length === 0)
114
+ return NONE;
115
+ const targets = resolveIncidentWebhooks(readFileConfig(kyaHome(env)));
116
+ // Claim under the lock so two flush children never send the same alert.
117
+ const claimed = await withStateLock(env, () => {
118
+ const entries = readAlertsState(env);
119
+ const due = [];
120
+ for (const inc of incidents) {
121
+ const ref = incidentRef(inc.id);
122
+ if (!needsAlert(entries, { ref, severity: inc.severity, firstTs: inc.firstTs, lastTs: inc.lastTs }, now.getTime()))
123
+ continue;
124
+ const alert = buildIncidentAlert(inc, events, briefs.get(inc.id));
125
+ const key = `${ref}:${inc.severity}`;
126
+ const prev = entries.find((e) => e.key === key);
127
+ // A pending entry is a retry of the same alert (delivery failed). A sent one means this is a new occurrence.
128
+ const retry = prev?.status === "pending";
129
+ const entry = {
130
+ key,
131
+ ref,
132
+ severity: inc.severity,
133
+ ts: now.toISOString(),
134
+ lastTs: inc.lastTs,
135
+ title: alert.title,
136
+ detectors: [...alert.detectorIds],
137
+ delivered: 0,
138
+ attempts: (prev?.attempts ?? 0) + 1,
139
+ ...(retry && prev?.triage ? { triage: prev.triage } : {}),
140
+ // No receiver: the alert shows in the report only, nothing more to retry.
141
+ status: targets.length === 0 ? "sent" : "pending",
142
+ };
143
+ if (prev)
144
+ entries.splice(entries.indexOf(prev), 1);
145
+ entries.push(entry);
146
+ due.push({ alert: retry && prev?.triage ? { ...alert, triage: prev.triage } : alert, key, inc, first: !retry });
147
+ }
148
+ if (due.length > 0)
149
+ writeAlertsState(env, entries);
150
+ return due;
151
+ });
152
+ if (!claimed || claimed.length === 0)
153
+ return NONE;
154
+ // Desktop first: its text holds no triage, and a crash of the model code (native) must not take the notification with it.
155
+ if (cfg.desktop) {
156
+ const fresh = claimed.filter((c) => c.first).slice(0, MAX_DESKTOP_PER_SCAN);
157
+ for (const { alert } of fresh)
158
+ await desktopNotify(alert, env, deps.desktop);
159
+ }
160
+ await addTriage(claimed, events, env, deps);
161
+ // Keep the triage before anything is sent: a retry then reuses it and never needs the model.
162
+ if (claimed.some((c) => c.first && c.alert.triage))
163
+ await saveTriage(env, claimed);
164
+ let delivered = 0;
165
+ const results = new Map();
166
+ if (targets.length > 0) {
167
+ for (const { alert, key } of claimed) {
168
+ const n = await notifyIncident(alert, targets, env, deps);
169
+ results.set(key, n);
170
+ delivered += n;
171
+ }
172
+ }
173
+ if (targets.length > 0) {
174
+ await withStateLock(env, () => {
175
+ const next = readAlertsState(env).map((e) => {
176
+ const n = results.get(e.key);
177
+ return n === undefined ? e : { ...e, delivered: n, status: n > 0 ? "sent" : "pending" };
178
+ });
179
+ writeAlertsState(env, next);
180
+ });
181
+ }
182
+ return { alerted: claimed.map((c) => c.alert), delivered };
183
+ }
184
+ catch {
185
+ return NONE;
186
+ }
187
+ }
188
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/alerts/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { Severity } from "../investigate/detectors.js";
2
+ import type { IncidentTriage } from "./triage-model.js";
3
+ export declare const ALERTS_STATE_FILE = "alerts-state.json";
4
+ export declare const ALERTS_STATE_KEEP = 500;
5
+ /** A failed delivery is tried again at most this many times in total. */
6
+ export declare const MAX_ATTEMPTS = 3;
7
+ /** A pending entry is not retried before this many ms have passed (a running sibling child owns it). */
8
+ export declare const RETRY_AFTER_MS = 60000;
9
+ export interface AlertEntry {
10
+ /** `${ref}:${severity}` */
11
+ readonly key: string;
12
+ /** Hash of the detector incident id. The raw id can hold a session id, so it is never stored. */
13
+ readonly ref: string;
14
+ readonly severity: Severity;
15
+ /** When the alert was claimed (ISO). */
16
+ readonly ts: string;
17
+ /** Last event time of the alerted incident (ISO, "" when unknown). */
18
+ readonly lastTs: string;
19
+ readonly title: string;
20
+ readonly detectors: readonly string[];
21
+ /** Receivers that accepted the alert. */
22
+ readonly delivered: number;
23
+ readonly attempts: number;
24
+ readonly status: "sent" | "pending";
25
+ /** Local model triage of this incident, when the model was installed. Advisory. */
26
+ readonly triage?: IncidentTriage;
27
+ }
28
+ /** Defensive parse: every id is checked against a plain-word shape, so a forged state file cannot inject markup. */
29
+ export declare function parseTriage(raw: unknown): IncidentTriage | undefined;
30
+ export declare function incidentRef(incidentId: string): string;
31
+ export declare function alertsStatePath(env?: NodeJS.ProcessEnv): string;
32
+ /** Missing or bad file: an empty list (alerting then works as on a first run). */
33
+ export declare function readAlertsState(env?: NodeJS.ProcessEnv): AlertEntry[];
34
+ /** Atomic write, last ALERTS_STATE_KEEP entries. Never throws. */
35
+ export declare function writeAlertsState(env: NodeJS.ProcessEnv, entries: readonly AlertEntry[]): void;
36
+ export interface IncidentMeta {
37
+ readonly ref: string;
38
+ readonly severity: Severity;
39
+ readonly firstTs: string;
40
+ readonly lastTs: string;
41
+ }
42
+ /**
43
+ * Pure dedupe decision. `entries` is the stored list in write order.
44
+ * Alert when: no entry for the ref; severity above the last entry; the incident starts after the
45
+ * last alerted one ended (new occurrence); or the last entry is still pending (not delivered),
46
+ * has attempts left, and its cool-down has passed.
47
+ */
48
+ export declare function needsAlert(entries: readonly AlertEntry[], inc: IncidentMeta, nowMs: number): boolean;
49
+ /**
50
+ * Run `fn` while holding the state lock (an exclusive mkdir). A lock older than 15 s is stale and
51
+ * removed. When the lock cannot be taken within 2 s, `fn` is skipped and undefined is returned:
52
+ * the next flush or the next report tick covers it.
53
+ */
54
+ export declare function withStateLock<T>(env: NodeJS.ProcessEnv, fn: () => T | Promise<T>): Promise<T | undefined>;
55
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/alerts/state.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,mBAAmB,CAAC;AAEpE,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AACrD,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,yEAAyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,wGAAwG;AACxG,eAAO,MAAM,cAAc,QAAS,CAAC;AAIrC,MAAM,WAAW,UAAU;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iGAAiG;IACjG,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CAClC;AAWD,oHAAoH;AACpH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CASpE;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE5E;AAgCD,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,UAAU,EAAE,CAUlF;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAQ7F;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAapG;AAID;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAgC/G"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Dedupe memory for incident alerts: <kya-home>/.kya/alerts-state.json.
3
+ * One entry per (incident ref, severity). An incident alerts once. It alerts again only when its
4
+ * severity rises, or when a new occurrence starts after the last alerted one ended. A refresh
5
+ * (more events in the same incident) never alerts again. The file keeps the last 500 entries
6
+ * and is written atomically. Several detached flush children can run at once, so the claim step
7
+ * runs under a short lock directory.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { globalConfigDir } from "../config.js";
13
+ import { atomicWriteSync } from "../fs-atomic.js";
14
+ import { isSeverity, severityRank } from "./config.js";
15
+ export const ALERTS_STATE_FILE = "alerts-state.json";
16
+ export const ALERTS_STATE_KEEP = 500;
17
+ /** A failed delivery is tried again at most this many times in total. */
18
+ export const MAX_ATTEMPTS = 3;
19
+ /** A pending entry is not retried before this many ms have passed (a running sibling child owns it). */
20
+ export const RETRY_AFTER_MS = 60_000;
21
+ const LOCK_STALE_MS = 15_000;
22
+ const LOCK_WAIT_MS = 2_000;
23
+ const TRIAGE_ID = /^[a-z0-9-]{1,40}$/;
24
+ function parsePick(raw) {
25
+ if (!raw || typeof raw !== "object")
26
+ return undefined;
27
+ const r = raw;
28
+ if (typeof r.id !== "string" || !TRIAGE_ID.test(r.id) || typeof r.p !== "number" || !Number.isFinite(r.p))
29
+ return undefined;
30
+ return { id: r.id, p: Math.min(1, Math.max(0, r.p)) };
31
+ }
32
+ /** Defensive parse: every id is checked against a plain-word shape, so a forged state file cannot inject markup. */
33
+ export function parseTriage(raw) {
34
+ if (!raw || typeof raw !== "object")
35
+ return undefined;
36
+ const r = raw;
37
+ const category = parsePick(r.category);
38
+ const urgency = parsePick(r.urgency);
39
+ const action = parsePick(r.action);
40
+ if (!category || !urgency || !action || typeof r.model !== "string" || !TRIAGE_ID.test(r.model))
41
+ return undefined;
42
+ if (typeof r.benign !== "number" || !Number.isFinite(r.benign))
43
+ return undefined;
44
+ return { model: r.model, category, urgency, action, benign: Math.min(1, Math.max(0, r.benign)) };
45
+ }
46
+ export function incidentRef(incidentId) {
47
+ return createHash("sha256").update(incidentId, "utf8").digest("hex").slice(0, 12);
48
+ }
49
+ export function alertsStatePath(env = process.env) {
50
+ return join(globalConfigDir(env), ALERTS_STATE_FILE);
51
+ }
52
+ function parseEntry(raw) {
53
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
54
+ return undefined;
55
+ const e = raw;
56
+ if (typeof e.key !== "string" ||
57
+ typeof e.ref !== "string" ||
58
+ !isSeverity(e.severity) ||
59
+ typeof e.ts !== "string" ||
60
+ typeof e.title !== "string" ||
61
+ (e.status !== "sent" && e.status !== "pending")) {
62
+ return undefined;
63
+ }
64
+ return {
65
+ key: e.key.slice(0, 80),
66
+ ref: e.ref.slice(0, 40),
67
+ severity: e.severity,
68
+ ts: e.ts.slice(0, 40),
69
+ lastTs: typeof e.lastTs === "string" ? e.lastTs.slice(0, 40) : "",
70
+ title: e.title.slice(0, 200),
71
+ detectors: Array.isArray(e.detectors)
72
+ ? e.detectors.filter((d) => typeof d === "string").slice(0, 10).map((d) => d.slice(0, 40))
73
+ : [],
74
+ delivered: typeof e.delivered === "number" && Number.isFinite(e.delivered) ? Math.max(0, Math.floor(e.delivered)) : 0,
75
+ attempts: typeof e.attempts === "number" && Number.isFinite(e.attempts) ? Math.max(0, Math.floor(e.attempts)) : 0,
76
+ status: e.status,
77
+ ...(parseTriage(e.triage) ? { triage: parseTriage(e.triage) } : {}),
78
+ };
79
+ }
80
+ /** Missing or bad file: an empty list (alerting then works as on a first run). */
81
+ export function readAlertsState(env = process.env) {
82
+ try {
83
+ const path = alertsStatePath(env);
84
+ if (!existsSync(path))
85
+ return [];
86
+ const raw = JSON.parse(readFileSync(path, "utf8"));
87
+ if (!Array.isArray(raw.entries))
88
+ return [];
89
+ return raw.entries.map(parseEntry).filter((e) => e !== undefined);
90
+ }
91
+ catch {
92
+ return [];
93
+ }
94
+ }
95
+ /** Atomic write, last ALERTS_STATE_KEEP entries. Never throws. */
96
+ export function writeAlertsState(env, entries) {
97
+ try {
98
+ mkdirSync(globalConfigDir(env), { recursive: true });
99
+ const kept = entries.slice(-ALERTS_STATE_KEEP);
100
+ atomicWriteSync(alertsStatePath(env), `${JSON.stringify({ version: 1, entries: kept }, null, 1)}\n`);
101
+ }
102
+ catch {
103
+ /* a lost write only means one alert may repeat */
104
+ }
105
+ }
106
+ /**
107
+ * Pure dedupe decision. `entries` is the stored list in write order.
108
+ * Alert when: no entry for the ref; severity above the last entry; the incident starts after the
109
+ * last alerted one ended (new occurrence); or the last entry is still pending (not delivered),
110
+ * has attempts left, and its cool-down has passed.
111
+ */
112
+ export function needsAlert(entries, inc, nowMs) {
113
+ let last;
114
+ for (const e of entries)
115
+ if (e.ref === inc.ref)
116
+ last = e;
117
+ if (last === undefined)
118
+ return true;
119
+ if (severityRank(inc.severity) > severityRank(last.severity))
120
+ return true;
121
+ const first = Date.parse(inc.firstTs);
122
+ const prevEnd = Date.parse(last.lastTs);
123
+ if (!Number.isNaN(first) && !Number.isNaN(prevEnd) && first > prevEnd)
124
+ return true;
125
+ if (last.status === "pending" && last.attempts < MAX_ATTEMPTS) {
126
+ const claimed = Date.parse(last.ts);
127
+ return Number.isNaN(claimed) || nowMs - claimed >= RETRY_AFTER_MS;
128
+ }
129
+ return false;
130
+ }
131
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
132
+ /**
133
+ * Run `fn` while holding the state lock (an exclusive mkdir). A lock older than 15 s is stale and
134
+ * removed. When the lock cannot be taken within 2 s, `fn` is skipped and undefined is returned:
135
+ * the next flush or the next report tick covers it.
136
+ */
137
+ export async function withStateLock(env, fn) {
138
+ const dir = `${alertsStatePath(env)}.lock`;
139
+ const deadline = Date.now() + LOCK_WAIT_MS;
140
+ try {
141
+ mkdirSync(globalConfigDir(env), { recursive: true });
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ for (;;) {
147
+ try {
148
+ mkdirSync(dir);
149
+ break;
150
+ }
151
+ catch (err) {
152
+ if (err.code !== "EEXIST")
153
+ return undefined;
154
+ try {
155
+ if (Date.now() - statSync(dir).mtimeMs > LOCK_STALE_MS)
156
+ rmSync(dir, { recursive: true, force: true });
157
+ }
158
+ catch {
159
+ /* the holder just released it */
160
+ }
161
+ if (Date.now() >= deadline)
162
+ return undefined;
163
+ await sleep(25);
164
+ }
165
+ }
166
+ try {
167
+ return await fn();
168
+ }
169
+ finally {
170
+ try {
171
+ rmSync(dir, { recursive: true, force: true });
172
+ }
173
+ catch {
174
+ /* stale lock cleanup handles it */
175
+ }
176
+ }
177
+ }
178
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/alerts/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { TrailEvent } from "../trail.js";
2
+ import { type Severity } from "../investigate/detectors.js";
3
+ import type { Incident } from "../investigate/incidents.js";
4
+ export declare const TRIAGE_SCHEMA_VERSION = "1";
5
+ export declare const MAX_REASON_CODES = 10;
6
+ export declare const MAX_NEVER_RULES = 10;
7
+ export declare const MAX_TARGET_CLASSES = 10;
8
+ export declare const MAX_TOOL_IDS = 10;
9
+ export declare const MAX_SUMMARIES = 5;
10
+ export declare const MAX_SUMMARY_CHARS = 120;
11
+ /** Clip for short identifier lists (tool ids, reason codes, rule ids, classes). */
12
+ export declare const MAX_ID_CHARS = 64;
13
+ /** Even the minimal input failed the secret check. The caller must skip the model and alert on detector severity. */
14
+ export declare class TriageInputUnsafeError extends Error {
15
+ constructor();
16
+ }
17
+ export interface TriageInput {
18
+ readonly schemaVersion: string;
19
+ readonly detectorIds: string[];
20
+ readonly severity: Severity;
21
+ readonly reasonCodes: string[];
22
+ readonly neverRules: string[];
23
+ readonly targetClasses: string[];
24
+ readonly toolIds: string[];
25
+ readonly counts: {
26
+ readonly allow: number;
27
+ readonly hold: number;
28
+ readonly deny: number;
29
+ };
30
+ readonly windowMinutes: number;
31
+ readonly summaries: string[];
32
+ readonly product: string;
33
+ readonly repeats: number;
34
+ }
35
+ export declare function buildTriageInput(incident: Incident, events: readonly TrailEvent[]): TriageInput;
36
+ //# sourceMappingURL=triage-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triage-input.d.ts","sourceRoot":"","sources":["../../src/alerts/triage-input.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAE5D,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,mFAAmF;AACnF,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,qHAAqH;AACrH,qBAAa,sBAAuB,SAAQ,KAAK;;CAKhD;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1F,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAqDD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,WAAW,CAwC/F"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Privacy-safe model input for incident triage. Built only from the detector
3
+ * incident and its trail events. Free text comes from TrailEvent.summary,
4
+ * re-redacted and clipped. Raw arguments, argument hashes, target paths, diff
5
+ * previews, project folders, session ids and device ids never enter the input.
6
+ * Mirrored in Java: com.shieldagent.core.kya.alerts.TriageInputBuilder.
7
+ */
8
+ import { assertNoSecrets } from "../dash/render.js";
9
+ import { redactTrailText } from "../trail-summary.js";
10
+ import { containsPii } from "../investigate/detectors.js";
11
+ export const TRIAGE_SCHEMA_VERSION = "1";
12
+ export const MAX_REASON_CODES = 10;
13
+ export const MAX_NEVER_RULES = 10;
14
+ export const MAX_TARGET_CLASSES = 10;
15
+ export const MAX_TOOL_IDS = 10;
16
+ export const MAX_SUMMARIES = 5;
17
+ export const MAX_SUMMARY_CHARS = 120;
18
+ /** Clip for short identifier lists (tool ids, reason codes, rule ids, classes). */
19
+ export const MAX_ID_CHARS = 64;
20
+ /** Even the minimal input failed the secret check. The caller must skip the model and alert on detector severity. */
21
+ export class TriageInputUnsafeError extends Error {
22
+ constructor() {
23
+ super("triage input failed the secret check after every fallback stage");
24
+ this.name = "TriageInputUnsafeError";
25
+ }
26
+ }
27
+ const HOME_UNIX = /\/(?:Users|home)\/[^/\s]+/g;
28
+ const HOME_ROOT = /\/root(?=[/\s]|$)/g;
29
+ const HOME_WIN = /[A-Za-z]:\\Users\\[^\\\s]+/g;
30
+ /** Control, format and line or paragraph separator characters (U+2028, U+2029, U+200B, U+FEFF ...). */
31
+ const INVISIBLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
32
+ /** Every event identifier is untrusted: only this shape reaches the model (whole value, no spaces). */
33
+ const SAFE_ID = /^[A-Za-z0-9._:/*@+-]{1,64}$/;
34
+ /** Replace a home folder (and the user name in it) with "~". */
35
+ function stripHome(s) {
36
+ return s.replace(HOME_UNIX, "~").replace(HOME_ROOT, "~").replace(HOME_WIN, "~");
37
+ }
38
+ function clip(s, max) {
39
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
40
+ }
41
+ /**
42
+ * Free text: invisible characters become a space, then redact, remove home folders, flatten
43
+ * whitespace. The caller clips. Empty string means drop.
44
+ */
45
+ function flatten(s) {
46
+ const visible = s.replace(INVISIBLE, " ");
47
+ return stripHome(redactTrailText(visible)).replace(/\s+/g, " ").trim();
48
+ }
49
+ /** An identifier is kept only when it has the safe shape and redaction or home-path removal would not change it. */
50
+ function isSafeId(v) {
51
+ return SAFE_ID.test(v) && stripHome(redactTrailText(v)) === v;
52
+ }
53
+ /** First-seen order, only safe identifiers, no duplicates, at most `max` items. */
54
+ function uniq(values, max) {
55
+ const seen = new Set();
56
+ for (const v of values) {
57
+ if (typeof v === "string" && isSafeId(v))
58
+ seen.add(v);
59
+ if (seen.size >= max)
60
+ break;
61
+ }
62
+ return [...seen];
63
+ }
64
+ function incidentEvents(incident, events) {
65
+ const idx = new Set();
66
+ for (const f of incident.findings) {
67
+ for (const i of f.eventIdx) {
68
+ if (Number.isInteger(i) && i >= 0 && i < events.length)
69
+ idx.add(i);
70
+ }
71
+ }
72
+ return [...idx].sort((a, b) => a - b).map((i) => events[i]);
73
+ }
74
+ export function buildTriageInput(incident, events) {
75
+ const evs = incidentEvents(incident, events);
76
+ const counts = { allow: 0, hold: 0, deny: 0 };
77
+ const perTool = new Map();
78
+ const summaries = [];
79
+ let product = "";
80
+ for (const e of evs) {
81
+ if (e.verdict === "ALLOW")
82
+ counts.allow++;
83
+ else if (e.verdict === "REQUIRE_APPROVE")
84
+ counts.hold++;
85
+ else if (e.verdict === "DENY")
86
+ counts.deny++;
87
+ perTool.set(e.toolId, (perTool.get(e.toolId) ?? 0) + 1);
88
+ if (!product && e.product)
89
+ product = e.product;
90
+ if (e.summary && summaries.length < MAX_SUMMARIES) {
91
+ // A summary the pii-in-summary detector flags (e-mail, card number, high-entropy token) is dropped
92
+ // whole, before the clip: a clip could cut a match in two, and a partial mask is not a guarantee.
93
+ const flat = flatten(e.summary);
94
+ if (containsPii(e.summary) || containsPii(flat))
95
+ continue;
96
+ const s = clip(flat, MAX_SUMMARY_CHARS);
97
+ if (s && !summaries.includes(s))
98
+ summaries.push(s);
99
+ }
100
+ }
101
+ const first = Date.parse(incident.firstTs);
102
+ const last = Date.parse(incident.lastTs);
103
+ const windowMinutes = Number.isNaN(first) || Number.isNaN(last) ? 0 : Math.max(0, Math.ceil((last - first) / 60_000));
104
+ const input = {
105
+ schemaVersion: TRIAGE_SCHEMA_VERSION,
106
+ detectorIds: [...new Set(incident.findings.map((f) => f.detectorId).filter(isSafeId))].sort(),
107
+ severity: incident.severity,
108
+ reasonCodes: uniq(evs.map((e) => e.reasonCode), MAX_REASON_CODES),
109
+ neverRules: uniq(evs.map((e) => e.neverRule), MAX_NEVER_RULES),
110
+ targetClasses: uniq(evs.map((e) => e.targetClass), MAX_TARGET_CLASSES),
111
+ toolIds: uniq(evs.map((e) => e.toolId), MAX_TOOL_IDS),
112
+ counts,
113
+ windowMinutes,
114
+ summaries,
115
+ product: isSafeId(product) ? product : "unknown",
116
+ repeats: Math.max(0, ...perTool.values()),
117
+ };
118
+ return failClosed(input);
119
+ }
120
+ function passesSecretCheck(input) {
121
+ try {
122
+ assertNoSecrets(JSON.stringify(input));
123
+ return true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ /**
130
+ * Check after every stage: full input, then no summaries, then also no tool ids, then the minimal input
131
+ * (no free-text field at all). Throws when even the minimal input fails, so alerting uses detector severity.
132
+ */
133
+ function failClosed(input) {
134
+ const noText = { ...input, summaries: [] };
135
+ const stages = [
136
+ input,
137
+ noText,
138
+ { ...noText, toolIds: [] },
139
+ {
140
+ schemaVersion: input.schemaVersion,
141
+ detectorIds: input.detectorIds,
142
+ severity: input.severity,
143
+ reasonCodes: [],
144
+ neverRules: [],
145
+ targetClasses: [],
146
+ toolIds: [],
147
+ counts: input.counts,
148
+ windowMinutes: input.windowMinutes,
149
+ summaries: [],
150
+ product: "unknown",
151
+ repeats: input.repeats,
152
+ },
153
+ ];
154
+ for (const stage of stages)
155
+ if (passesSecretCheck(stage))
156
+ return stage;
157
+ throw new TriageInputUnsafeError();
158
+ }
159
+ //# sourceMappingURL=triage-input.js.map