podwatch 1.0.2

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 (51) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +92 -0
  3. package/bin/podwatch.js +10 -0
  4. package/dist/classifier.d.ts +22 -0
  5. package/dist/classifier.d.ts.map +1 -0
  6. package/dist/classifier.js +157 -0
  7. package/dist/classifier.js.map +1 -0
  8. package/dist/hooks/cost.d.ts +26 -0
  9. package/dist/hooks/cost.d.ts.map +1 -0
  10. package/dist/hooks/cost.js +107 -0
  11. package/dist/hooks/cost.js.map +1 -0
  12. package/dist/hooks/lifecycle.d.ts +16 -0
  13. package/dist/hooks/lifecycle.d.ts.map +1 -0
  14. package/dist/hooks/lifecycle.js +273 -0
  15. package/dist/hooks/lifecycle.js.map +1 -0
  16. package/dist/hooks/security.d.ts +19 -0
  17. package/dist/hooks/security.d.ts.map +1 -0
  18. package/dist/hooks/security.js +128 -0
  19. package/dist/hooks/security.js.map +1 -0
  20. package/dist/hooks/sessions.d.ts +10 -0
  21. package/dist/hooks/sessions.d.ts.map +1 -0
  22. package/dist/hooks/sessions.js +53 -0
  23. package/dist/hooks/sessions.js.map +1 -0
  24. package/dist/index.d.ts +32 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +120 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/redact.d.ts +35 -0
  29. package/dist/redact.d.ts.map +1 -0
  30. package/dist/redact.js +372 -0
  31. package/dist/redact.js.map +1 -0
  32. package/dist/scanner.d.ts +27 -0
  33. package/dist/scanner.d.ts.map +1 -0
  34. package/dist/scanner.js +117 -0
  35. package/dist/scanner.js.map +1 -0
  36. package/dist/transmitter.d.ts +58 -0
  37. package/dist/transmitter.d.ts.map +1 -0
  38. package/dist/transmitter.js +654 -0
  39. package/dist/transmitter.js.map +1 -0
  40. package/dist/types.d.ts +116 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +9 -0
  43. package/dist/types.js.map +1 -0
  44. package/dist/updater.d.ts +168 -0
  45. package/dist/updater.d.ts.map +1 -0
  46. package/dist/updater.js +579 -0
  47. package/dist/updater.js.map +1 -0
  48. package/lib/installer.js +599 -0
  49. package/openclaw.plugin.json +59 -0
  50. package/package.json +56 -0
  51. package/skills/podwatch/SKILL.md +112 -0
@@ -0,0 +1,654 @@
1
+ "use strict";
2
+ /**
3
+ * Batched HTTP transmitter — queues events and flushes to Podwatch cloud.
4
+ *
5
+ * Features:
6
+ * - Configurable batch size and flush interval
7
+ * - Exponential backoff on failure
8
+ * - Buffer overflow protection (drops SAFE events first)
9
+ * - Graceful shutdown (flush on gateway_stop)
10
+ * - Credential access tracking (for exfiltration detection)
11
+ * - Known tool tracking (for first-time tool alerts)
12
+ * - Cached budget state (synced every 60s from dashboard)
13
+ * - Local audit log for dropped events
14
+ * - 402 trial-expired handling
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.transmitter = void 0;
51
+ const fs = __importStar(require("node:fs"));
52
+ const path = __importStar(require("node:path"));
53
+ const os = __importStar(require("node:os"));
54
+ // Read version from package.json at build time (resolveJsonModule enabled)
55
+ const PLUGIN_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")).version;
56
+ // ---------------------------------------------------------------------------
57
+ // State
58
+ // ---------------------------------------------------------------------------
59
+ let config = null;
60
+ let buffer = [];
61
+ let flushTimer = null;
62
+ let flushInProgress = false;
63
+ let retryBackoffMs = 1_000;
64
+ let retryCount = 0;
65
+ const MAX_BACKOFF_MS = 30_000;
66
+ const MAX_RETRIES = 10;
67
+ const MAX_BUFFER_SIZE = 1_000;
68
+ const BUFFER_TARGET_SIZE = 900;
69
+ // --- Trial expired flag ---
70
+ let trialExpired = false;
71
+ const recentCredentialAccesses = [];
72
+ const CREDENTIAL_ACCESS_WINDOW_MS = 60_000; // 60 seconds
73
+ // --- Known tools tracking ---
74
+ const knownTools = new Set();
75
+ const KNOWN_TOOLS_MAX_SIZE = 10_000;
76
+ let activateTs = 0; // when the plugin started
77
+ let cachedBudget = null;
78
+ let budgetSyncTimer = null;
79
+ const BUDGET_SYNC_INTERVAL_MS = 300_000; // 5 minutes — acceptable with 95% tolerance buffer
80
+ // ---------------------------------------------------------------------------
81
+ // Audit log for dropped events
82
+ // ---------------------------------------------------------------------------
83
+ function getAuditLogPath() {
84
+ return path.join(os.homedir(), ".openclaw", "extensions", "podwatch", "audit.log");
85
+ }
86
+ const AUDIT_LOG_MAX_BYTES = 1_048_576; // 1 MB
87
+ function writeAuditLog(reason, eventCount, events) {
88
+ try {
89
+ const logPath = getAuditLogPath();
90
+ const logDir = path.dirname(logPath);
91
+ fs.mkdirSync(logDir, { recursive: true });
92
+ // Enforce size cap — rotate if at or over 1 MB
93
+ try {
94
+ const stats = fs.statSync(logPath);
95
+ if (stats.size >= AUDIT_LOG_MAX_BYTES) {
96
+ // Truncate: keep the last ~half of the file
97
+ const content = fs.readFileSync(logPath, "utf-8");
98
+ const half = Math.floor(content.length / 2);
99
+ // Find the first newline after the midpoint to avoid partial lines
100
+ const cutIdx = content.indexOf("\n", half);
101
+ const trimmed = cutIdx >= 0 ? content.slice(cutIdx + 1) : "";
102
+ fs.writeFileSync(logPath, trimmed, { mode: 0o600 });
103
+ }
104
+ }
105
+ catch {
106
+ // File may not exist yet — that's fine, appendFileSync will create it
107
+ }
108
+ const entry = JSON.stringify({
109
+ ts: new Date().toISOString(),
110
+ reason,
111
+ eventCount,
112
+ eventTypes: events.map((e) => e.type),
113
+ });
114
+ fs.appendFileSync(logPath, entry + "\n", { mode: 0o600 });
115
+ // Ensure file permissions are 0o600 (owner read/write only)
116
+ fs.chmodSync(logPath, 0o600);
117
+ }
118
+ catch {
119
+ // Best-effort — don't crash the plugin over audit logging
120
+ }
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // HTTP flush
124
+ // ---------------------------------------------------------------------------
125
+ /**
126
+ * Map internal event type to a resultStatus string that the dashboard API expects.
127
+ */
128
+ function mapResultStatus(event) {
129
+ switch (event.type) {
130
+ case "tool_call":
131
+ return "invoked";
132
+ case "tool_result":
133
+ return event.success === false ? "error" : "success";
134
+ case "cost":
135
+ return "usage";
136
+ case "security":
137
+ return "alert";
138
+ case "budget_blocked":
139
+ return "blocked";
140
+ case "session_start":
141
+ return "session_start";
142
+ case "session_end":
143
+ return "session_end";
144
+ case "heartbeat":
145
+ return "heartbeat";
146
+ case "compaction":
147
+ return "compaction";
148
+ case "scan":
149
+ return "scan";
150
+ case "setup_warning":
151
+ return "warning";
152
+ case "alert":
153
+ return "alert";
154
+ default:
155
+ return event.type || "unknown";
156
+ }
157
+ }
158
+ /**
159
+ * Build a human-readable description from the internal event fields.
160
+ */
161
+ function buildDescription(event) {
162
+ const parts = [];
163
+ switch (event.type) {
164
+ case "tool_call": {
165
+ const name = event.toolName ? String(event.toolName) : "unknown_tool";
166
+ parts.push(`Called: ${name}`);
167
+ break;
168
+ }
169
+ case "tool_result": {
170
+ const name = event.toolName ? String(event.toolName) : "unknown_tool";
171
+ const status = event.success === false ? "error" : "success";
172
+ parts.push(`${name}: ${status}`);
173
+ if (event.error)
174
+ parts.push(String(event.error).slice(0, 200));
175
+ break;
176
+ }
177
+ case "cost": {
178
+ const model = event.model ? String(event.model) : "unknown_model";
179
+ const cost = typeof event.costUsd === "number" ? `$${event.costUsd.toFixed(4)}` : "$?";
180
+ const tokens = typeof event.totalTokens === "number" ? `${event.totalTokens} tokens` : "? tokens";
181
+ parts.push(`${model}: ${cost} (${tokens})`);
182
+ break;
183
+ }
184
+ case "security": {
185
+ if (event.pattern)
186
+ parts.push(String(event.pattern));
187
+ if (event.severity)
188
+ parts.push(`severity=${event.severity}`);
189
+ if (event.reason)
190
+ parts.push(String(event.reason));
191
+ break;
192
+ }
193
+ case "session_start": {
194
+ parts.push("Session started");
195
+ if (event.sessionId)
196
+ parts.push(`id=${event.sessionId}`);
197
+ break;
198
+ }
199
+ case "session_end": {
200
+ parts.push("Session ended");
201
+ if (event.sessionId)
202
+ parts.push(`id=${event.sessionId}`);
203
+ if (typeof event.durationMs === "number")
204
+ parts.push(`duration=${Math.round(Number(event.durationMs) / 1000)}s`);
205
+ break;
206
+ }
207
+ default: {
208
+ if (event.type)
209
+ parts.push(String(event.type));
210
+ if (event.toolName)
211
+ parts.push(String(event.toolName));
212
+ if (event.pattern)
213
+ parts.push(String(event.pattern));
214
+ if (event.severity)
215
+ parts.push(`severity=${event.severity}`);
216
+ if (event.reason)
217
+ parts.push(String(event.reason));
218
+ if (event.message)
219
+ parts.push(String(event.message));
220
+ if (event.error)
221
+ parts.push(`error: ${String(event.error).slice(0, 200)}`);
222
+ if (event.loopDetected)
223
+ parts.push(`loop_detected (${event.messagesPerMinute} msg/min)`);
224
+ break;
225
+ }
226
+ }
227
+ const desc = parts.join(": ");
228
+ return desc.slice(0, 2000);
229
+ }
230
+ /**
231
+ * Determine sessionType from the internal event.
232
+ * If the event already carries an explicit sessionType (e.g. set by cost handler
233
+ * for heartbeat-triggered LLM calls), use it directly.
234
+ */
235
+ function mapSessionType(event) {
236
+ // Allow upstream hooks to pre-set sessionType (e.g. heartbeat cost events)
237
+ if (typeof event.sessionType === "string" && event.sessionType) {
238
+ return event.sessionType;
239
+ }
240
+ if (event.type === "heartbeat")
241
+ return "heartbeat";
242
+ if (event.type === "scan")
243
+ return "cron";
244
+ // Tool calls, tool results, cost, security, session events = interactive
245
+ if (event.type === "tool_call" || event.type === "tool_result" || event.type === "cost" ||
246
+ event.type === "security" || event.type === "budget_blocked" ||
247
+ event.type === "session_start" || event.type === "session_end" ||
248
+ event.type === "compaction" || event.type === "alert")
249
+ return "interactive";
250
+ return "unknown";
251
+ }
252
+ /**
253
+ * Extract agentId from a sessionKey string.
254
+ * SessionKey format: "agent:<agentId>:<sessionType>:..."
255
+ */
256
+ function extractAgentIdFromSessionKey(sessionKey) {
257
+ if (!sessionKey || typeof sessionKey !== "string")
258
+ return undefined;
259
+ const parts = sessionKey.split(":");
260
+ // Format: agent:<agentId>:<rest...>
261
+ if (parts.length >= 2 && parts[0] === "agent") {
262
+ return parts[1] || undefined;
263
+ }
264
+ return undefined;
265
+ }
266
+ /**
267
+ * Resolve agentId with fallback chain:
268
+ * 1. event.agentId (if present)
269
+ * 2. Extract from event.sessionKey
270
+ * 3. Default to "main"
271
+ */
272
+ function resolveAgentId(event) {
273
+ if (event.agentId != null && String(event.agentId).length > 0) {
274
+ return String(event.agentId);
275
+ }
276
+ const sessionKey = (event.sessionKey ?? event.sessionId);
277
+ if (sessionKey) {
278
+ const extracted = extractAgentIdFromSessionKey(sessionKey);
279
+ if (extracted)
280
+ return extracted;
281
+ }
282
+ return "main";
283
+ }
284
+ /**
285
+ * Resolve sessionId with fallback chain:
286
+ * 1. event.sessionKey (if present)
287
+ * 2. event.sessionId
288
+ * 3. Default to "default"
289
+ */
290
+ function resolveSessionId(event) {
291
+ if (event.sessionKey != null && String(event.sessionKey).length > 0) {
292
+ return String(event.sessionKey);
293
+ }
294
+ if (event.sessionId != null && String(event.sessionId).length > 0) {
295
+ return String(event.sessionId);
296
+ }
297
+ return "default";
298
+ }
299
+ /**
300
+ * Resolve toolName — for cost events, use the model name instead of generic "cost".
301
+ */
302
+ function resolveToolName(event) {
303
+ if (event.type === "cost" && typeof event.model === "string" && event.model) {
304
+ return event.model;
305
+ }
306
+ return (typeof event.toolName === "string" && event.toolName) || event.type || "system";
307
+ }
308
+ /**
309
+ * Transform internal PodwatchEvent[] into the API-expected EventPayload[].
310
+ */
311
+ function transformEvents(events) {
312
+ return events.map((event) => {
313
+ const transformed = {
314
+ eventId: typeof event.eventId === "string" ? event.eventId : crypto.randomUUID(),
315
+ timestamp: new Date(event.ts).toISOString(),
316
+ toolName: resolveToolName(event),
317
+ resultStatus: mapResultStatus(event),
318
+ description: buildDescription(event),
319
+ sessionType: mapSessionType(event),
320
+ agentId: resolveAgentId(event),
321
+ sessionId: resolveSessionId(event),
322
+ };
323
+ // Optional fields — only include if present
324
+ if (event.model != null)
325
+ transformed.model = String(event.model);
326
+ if (typeof event.inputTokens === "number")
327
+ transformed.inputTokens = event.inputTokens;
328
+ if (typeof event.outputTokens === "number")
329
+ transformed.outputTokens = event.outputTokens;
330
+ if (typeof event.cacheReadTokens === "number")
331
+ transformed.cacheReadTokens = event.cacheReadTokens;
332
+ if (typeof event.cacheWriteTokens === "number")
333
+ transformed.cacheWriteTokens = event.cacheWriteTokens;
334
+ if (typeof event.durationMs === "number")
335
+ transformed.durationMs = event.durationMs;
336
+ if (event.params != null)
337
+ transformed.toolArgs = event.params;
338
+ if (typeof event.redactedCount === "number")
339
+ transformed.redactedCount = event.redactedCount;
340
+ return transformed;
341
+ });
342
+ }
343
+ async function sendBatch(events) {
344
+ if (!config)
345
+ return false;
346
+ const payload = {
347
+ events: transformEvents(events),
348
+ skillVersion: PLUGIN_VERSION,
349
+ };
350
+ try {
351
+ const response = await fetch(`${config.endpoint}/events`, {
352
+ method: "POST",
353
+ headers: {
354
+ "Content-Type": "application/json",
355
+ Authorization: `Bearer ${config.apiKey}`,
356
+ },
357
+ body: JSON.stringify(payload),
358
+ signal: AbortSignal.timeout(30_000),
359
+ });
360
+ if (response.ok) {
361
+ retryBackoffMs = 1_000;
362
+ return true;
363
+ }
364
+ // 402 — trial expired, disable all future transmissions
365
+ if (response.status === 402) {
366
+ trialExpired = true;
367
+ console.warn("[podwatch] Trial expired (402) — event transmission disabled. Visit podwatch.app to upgrade.");
368
+ return true; // consume the batch (don't retry)
369
+ }
370
+ // Other 4xx — client error, drop events + audit log
371
+ if (response.status >= 400 && response.status < 500) {
372
+ console.error(`[podwatch] API ${response.status}: dropping ${events.length} events`);
373
+ writeAuditLog(`http_${response.status}`, events.length, events);
374
+ retryBackoffMs = 1_000;
375
+ return true; // Don't retry client errors
376
+ }
377
+ console.error(`[podwatch] API ${response.status}: will retry ${events.length} events`);
378
+ return false;
379
+ }
380
+ catch (err) {
381
+ console.error("[podwatch] Network error during flush:", err);
382
+ return false;
383
+ }
384
+ }
385
+ // ---------------------------------------------------------------------------
386
+ // Core flush
387
+ // ---------------------------------------------------------------------------
388
+ async function doFlush() {
389
+ if (flushInProgress || buffer.length === 0 || !config)
390
+ return;
391
+ // Trial expired — silently discard all buffered events
392
+ if (trialExpired) {
393
+ buffer.length = 0;
394
+ return;
395
+ }
396
+ flushInProgress = true;
397
+ try {
398
+ const batchSize = Math.min(buffer.length, config.batchSize);
399
+ const batch = buffer.slice(0, batchSize);
400
+ const success = await sendBatch(batch);
401
+ if (success) {
402
+ buffer.splice(0, batchSize);
403
+ retryBackoffMs = 1_000;
404
+ retryCount = 0;
405
+ }
406
+ else {
407
+ retryCount++;
408
+ // Drop batch only after exhausting all retries
409
+ if (retryCount >= MAX_RETRIES) {
410
+ writeAuditLog("max_retries_exceeded", batch.length, batch);
411
+ buffer.splice(0, batchSize);
412
+ retryBackoffMs = 1_000;
413
+ retryCount = 0;
414
+ }
415
+ else {
416
+ // Keep events in buffer (don't splice) — retry on next flush
417
+ retryBackoffMs = Math.min(retryBackoffMs * 2, MAX_BACKOFF_MS);
418
+ }
419
+ }
420
+ }
421
+ catch (err) {
422
+ console.error("[podwatch] Unexpected flush error:", err);
423
+ }
424
+ finally {
425
+ flushInProgress = false;
426
+ }
427
+ }
428
+ // ---------------------------------------------------------------------------
429
+ // Budget sync
430
+ // ---------------------------------------------------------------------------
431
+ /**
432
+ * Sync budget from dashboard API. Called on activate + every 300s.
433
+ * Stale by up to 5 min — acceptable with 95% tolerance buffer.
434
+ * Gracefully handles 404 (endpoint not deployed yet) without spamming logs.
435
+ */
436
+ let budgetSyncFailures = 0;
437
+ const BUDGET_SYNC_MAX_SILENT_FAILURES = 3; // Only warn after N consecutive failures
438
+ async function syncBudget() {
439
+ if (!config)
440
+ return;
441
+ try {
442
+ const response = await fetch(`${config.endpoint}/budget`, {
443
+ method: "GET",
444
+ headers: {
445
+ "Content-Type": "application/json",
446
+ Authorization: `Bearer ${config.apiKey}`,
447
+ },
448
+ signal: AbortSignal.timeout(10_000),
449
+ });
450
+ if (response.ok) {
451
+ const data = (await response.json());
452
+ cachedBudget = {
453
+ limit: typeof data.limit === "number" ? data.limit : 0,
454
+ currentSpend: typeof data.currentSpend === "number" ? data.currentSpend : 0,
455
+ lastSyncTs: Date.now(),
456
+ };
457
+ budgetSyncFailures = 0;
458
+ return;
459
+ }
460
+ // 404 — endpoint not deployed yet, silently ignore
461
+ if (response.status === 404) {
462
+ budgetSyncFailures++;
463
+ if (budgetSyncFailures === BUDGET_SYNC_MAX_SILENT_FAILURES) {
464
+ console.warn("[podwatch] Budget endpoint not available (404). Budget sync disabled until endpoint is deployed.");
465
+ }
466
+ return;
467
+ }
468
+ budgetSyncFailures++;
469
+ if (budgetSyncFailures <= BUDGET_SYNC_MAX_SILENT_FAILURES) {
470
+ console.warn(`[podwatch] Budget sync failed: HTTP ${response.status}`);
471
+ }
472
+ }
473
+ catch (err) {
474
+ budgetSyncFailures++;
475
+ if (budgetSyncFailures <= BUDGET_SYNC_MAX_SILENT_FAILURES) {
476
+ console.warn("[podwatch] Budget sync network error:", err);
477
+ }
478
+ }
479
+ }
480
+ // ---------------------------------------------------------------------------
481
+ // Public API
482
+ // ---------------------------------------------------------------------------
483
+ exports.transmitter = {
484
+ start(cfg) {
485
+ config = cfg;
486
+ buffer = [];
487
+ retryBackoffMs = 1_000;
488
+ retryCount = 0;
489
+ flushInProgress = false;
490
+ trialExpired = false;
491
+ activateTs = Date.now();
492
+ knownTools.clear();
493
+ recentCredentialAccesses.length = 0;
494
+ cachedBudget = null;
495
+ budgetSyncFailures = 0;
496
+ if (flushTimer)
497
+ clearInterval(flushTimer);
498
+ flushTimer = setInterval(() => void doFlush(), config.flushIntervalMs);
499
+ if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) {
500
+ flushTimer.unref();
501
+ }
502
+ // Start budget sync
503
+ void syncBudget();
504
+ if (budgetSyncTimer)
505
+ clearInterval(budgetSyncTimer);
506
+ budgetSyncTimer = setInterval(() => void syncBudget(), BUDGET_SYNC_INTERVAL_MS);
507
+ if (budgetSyncTimer && typeof budgetSyncTimer === "object" && "unref" in budgetSyncTimer) {
508
+ budgetSyncTimer.unref();
509
+ }
510
+ },
511
+ enqueue(event) {
512
+ buffer.push(event);
513
+ // Overflow protection — drop non-critical events down to BUFFER_TARGET_SIZE
514
+ if (buffer.length > MAX_BUFFER_SIZE) {
515
+ const criticalTypes = new Set(["setup_warning", "security", "budget_blocked"]);
516
+ const dropCount = buffer.length - BUFFER_TARGET_SIZE;
517
+ // Collect indices of non-critical events (oldest first)
518
+ const nonCriticalIndices = [];
519
+ for (let i = 0; i < buffer.length && nonCriticalIndices.length < dropCount; i++) {
520
+ if (!criticalTypes.has(buffer[i].type)) {
521
+ nonCriticalIndices.push(i);
522
+ }
523
+ }
524
+ // If not enough non-critical events, also drop critical (oldest first)
525
+ if (nonCriticalIndices.length < dropCount) {
526
+ for (let i = 0; i < buffer.length && nonCriticalIndices.length < dropCount; i++) {
527
+ if (criticalTypes.has(buffer[i].type)) {
528
+ nonCriticalIndices.push(i);
529
+ }
530
+ }
531
+ }
532
+ // Remove in reverse order to preserve indices
533
+ const dropped = [];
534
+ const indicesToDrop = nonCriticalIndices.sort((a, b) => b - a);
535
+ for (const idx of indicesToDrop) {
536
+ dropped.push(buffer[idx]);
537
+ buffer.splice(idx, 1);
538
+ }
539
+ if (dropped.length > 0) {
540
+ writeAuditLog("buffer_overflow", dropped.length, dropped);
541
+ console.warn(`[podwatch] Buffer overflow: dropped ${dropped.length} events (target: ${BUFFER_TARGET_SIZE})`);
542
+ }
543
+ }
544
+ // Flush if batch size reached
545
+ if (config && buffer.length >= config.batchSize) {
546
+ void doFlush();
547
+ }
548
+ },
549
+ async flush() {
550
+ await doFlush();
551
+ },
552
+ stop() {
553
+ if (flushTimer) {
554
+ clearInterval(flushTimer);
555
+ flushTimer = null;
556
+ }
557
+ if (budgetSyncTimer) {
558
+ clearInterval(budgetSyncTimer);
559
+ budgetSyncTimer = null;
560
+ }
561
+ },
562
+ /** Flush remaining events and stop. For graceful shutdown. */
563
+ async shutdown() {
564
+ while (buffer.length > 0 && config) {
565
+ await doFlush();
566
+ }
567
+ this.stop();
568
+ },
569
+ // -----------------------------------------------------------------------
570
+ // Credential access tracking (exfiltration detection)
571
+ // -----------------------------------------------------------------------
572
+ /** Mark that a tool accessed credentials. */
573
+ markCredentialAccess(toolName, params) {
574
+ const path = (typeof params.path === "string" && params.path) ||
575
+ (typeof params.file_path === "string" && params.file_path) ||
576
+ (typeof params.command === "string" && params.command) ||
577
+ "unknown";
578
+ recentCredentialAccesses.push({
579
+ toolName,
580
+ path,
581
+ ts: Date.now(),
582
+ });
583
+ // Prune old entries
584
+ this._pruneCredentialAccesses();
585
+ },
586
+ /** Check if there was a credential access within the last `windowSec` seconds. */
587
+ hasRecentCredentialAccess(windowSec) {
588
+ this._pruneCredentialAccesses();
589
+ const cutoff = Date.now() - windowSec * 1_000;
590
+ return recentCredentialAccesses.some((a) => a.ts >= cutoff);
591
+ },
592
+ /** Get the most recent credential access details (for exfiltration alerts). */
593
+ getRecentCredentialAccess(windowSec) {
594
+ this._pruneCredentialAccesses();
595
+ const cutoff = Date.now() - windowSec * 1_000;
596
+ const recent = recentCredentialAccesses.filter((a) => a.ts >= cutoff);
597
+ return recent.length > 0 ? recent[recent.length - 1] : null;
598
+ },
599
+ _pruneCredentialAccesses() {
600
+ const cutoff = Date.now() - CREDENTIAL_ACCESS_WINDOW_MS;
601
+ while (recentCredentialAccesses.length > 0 && recentCredentialAccesses[0].ts < cutoff) {
602
+ recentCredentialAccesses.shift();
603
+ }
604
+ },
605
+ // -----------------------------------------------------------------------
606
+ // Known tool tracking (first-time tool detection)
607
+ // -----------------------------------------------------------------------
608
+ /** Check if a tool has been seen before. */
609
+ isKnownTool(toolName) {
610
+ return knownTools.has(toolName);
611
+ },
612
+ /** Record a tool as seen. Clears the set when it exceeds KNOWN_TOOLS_MAX_SIZE. */
613
+ recordToolSeen(toolName) {
614
+ knownTools.add(toolName);
615
+ if (knownTools.size > KNOWN_TOOLS_MAX_SIZE) {
616
+ console.warn(`[podwatch] knownTools set exceeded ${KNOWN_TOOLS_MAX_SIZE} entries — resetting first-time-tool baseline`);
617
+ knownTools.clear();
618
+ knownTools.add(toolName);
619
+ }
620
+ },
621
+ /** Get agent uptime in hours since plugin activated. */
622
+ getAgentUptimeHours() {
623
+ if (activateTs === 0)
624
+ return 0;
625
+ return (Date.now() - activateTs) / (1_000 * 60 * 60);
626
+ },
627
+ // -----------------------------------------------------------------------
628
+ // Budget cache
629
+ // -----------------------------------------------------------------------
630
+ /** Get cached budget state (synced from dashboard every 60s). */
631
+ getCachedBudget() {
632
+ return cachedBudget;
633
+ },
634
+ /** Force a budget sync now. */
635
+ async forceBudgetSync() {
636
+ await syncBudget();
637
+ },
638
+ // -----------------------------------------------------------------------
639
+ // Diagnostics
640
+ // -----------------------------------------------------------------------
641
+ /** Current buffer size (for testing/diagnostics). */
642
+ get bufferedCount() {
643
+ return buffer.length;
644
+ },
645
+ /** Number of known tools (for testing/diagnostics). */
646
+ get knownToolCount() {
647
+ return knownTools.size;
648
+ },
649
+ /** Whether trial has expired (for testing). */
650
+ get isTrialExpired() {
651
+ return trialExpired;
652
+ },
653
+ };
654
+ //# sourceMappingURL=transmitter.js.map