forkit-connect 0.1.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 (51) hide show
  1. package/QUICKSTART.md +55 -0
  2. package/README.md +96 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +4724 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.js +21 -0
  7. package/dist/launcher.d.ts +33 -0
  8. package/dist/launcher.js +9344 -0
  9. package/dist/ps-list-loader.d.ts +5 -0
  10. package/dist/ps-list-loader.js +20 -0
  11. package/dist/v1/agent-observation.d.ts +42 -0
  12. package/dist/v1/agent-observation.js +499 -0
  13. package/dist/v1/api.d.ts +276 -0
  14. package/dist/v1/api.js +390 -0
  15. package/dist/v1/credential-store.d.ts +92 -0
  16. package/dist/v1/credential-store.js +797 -0
  17. package/dist/v1/currency.d.ts +41 -0
  18. package/dist/v1/currency.js +127 -0
  19. package/dist/v1/daemon.d.ts +50 -0
  20. package/dist/v1/daemon.js +265 -0
  21. package/dist/v1/discovery.d.ts +61 -0
  22. package/dist/v1/discovery.js +168 -0
  23. package/dist/v1/filesystem-models.d.ts +11 -0
  24. package/dist/v1/filesystem-models.js +261 -0
  25. package/dist/v1/heartbeat.d.ts +45 -0
  26. package/dist/v1/heartbeat.js +463 -0
  27. package/dist/v1/lifecycle-monitor.d.ts +78 -0
  28. package/dist/v1/lifecycle-monitor.js +512 -0
  29. package/dist/v1/lmstudio.d.ts +11 -0
  30. package/dist/v1/lmstudio.js +148 -0
  31. package/dist/v1/ollama.d.ts +19 -0
  32. package/dist/v1/ollama.js +164 -0
  33. package/dist/v1/openai-compatible.d.ts +12 -0
  34. package/dist/v1/openai-compatible.js +124 -0
  35. package/dist/v1/process-scout.d.ts +50 -0
  36. package/dist/v1/process-scout.js +715 -0
  37. package/dist/v1/providers.d.ts +50 -0
  38. package/dist/v1/providers.js +106 -0
  39. package/dist/v1/service.d.ts +680 -0
  40. package/dist/v1/service.js +8286 -0
  41. package/dist/v1/state.d.ts +87 -0
  42. package/dist/v1/state.js +1318 -0
  43. package/dist/v1/test-credential-backend.d.ts +19 -0
  44. package/dist/v1/test-credential-backend.js +49 -0
  45. package/dist/v1/types.d.ts +873 -0
  46. package/dist/v1/types.js +3 -0
  47. package/dist/v1/update.d.ts +38 -0
  48. package/dist/v1/update.js +184 -0
  49. package/dist/v1/vitality-pulse.d.ts +36 -0
  50. package/dist/v1/vitality-pulse.js +512 -0
  51. package/package.json +53 -0
@@ -0,0 +1,512 @@
1
+ "use strict";
2
+ /**
3
+ * lifecycle-monitor.ts
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ * Deep AI model and agent lifecycle observability engine for Forkit Connect.
6
+ *
7
+ * Capabilities (fully automatic, 24/7, login-only onboarding):
8
+ *
9
+ * 1. Silent model change detection
10
+ * Tracks digest, size, and weights-layer hash per model binding.
11
+ * Emits `connect_silent_model_change` when any of these change without an
12
+ * announced version event, queuing a PendingReview for human acknowledgement.
13
+ *
14
+ * 2. Model drift detection (metadata-based)
15
+ * Tracks quantization label and parameter count changes over scan cycles.
16
+ * Emits `connect_model_drift_detected` on capability-class changes.
17
+ *
18
+ * 3. Agent behavioral drift detection
19
+ * Computes a hash of each agent's distinct observed tool set. If the tool
20
+ * fingerprint changes significantly (tools added or removed) it emits
21
+ * `connect_agent_drift_detected`.
22
+ *
23
+ * 4. Prompt injection detection
24
+ * Scans agent tool-call observations for known injection patterns:
25
+ * system-override phrases, base64-encoded instruction payloads, DAN-style
26
+ * prompts. Emits `connect_prompt_injection_suspected` with confidence level.
27
+ *
28
+ * 5. Adversarial influence detection
29
+ * Correlates: (a) multiple injection signals within the same scan window,
30
+ * (b) a prompt injection AND a silent model change on the same agent/model
31
+ * pair. When correlated, emits `connect_adversarial_influence_observed`.
32
+ *
33
+ * Human-in-the-loop flow:
34
+ * Every detection writes a PendingReview. Humans can confirm/dismiss via
35
+ * `c2 reviews` CLI commands. Once a pattern is confirmed once, subsequent
36
+ * detections of the same kind for the same subject are auto-confirmed,
37
+ * giving full automation with initial oversight.
38
+ *
39
+ * Privacy contract:
40
+ * This module never reads prompt content or model outputs directly.
41
+ * It only inspects: model metadata (name, digest, size, quantization label,
42
+ * parameter count), agent tool names (not arguments), and detection signals
43
+ * already recorded in the evidence chain.
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.runLifecycleMonitoringPass = runLifecycleMonitoringPass;
47
+ exports.findPassportsNeedingApiKeyProvisioning = findPassportsNeedingApiKeyProvisioning;
48
+ const node_crypto_1 = require("node:crypto");
49
+ // ─── Constants ──────────────────────────────────────────────────────────────
50
+ /** Minimum seconds between monitor passes for the same subject. */
51
+ const MONITOR_COOLDOWN_SECONDS = 120;
52
+ /** Minimum size delta (bytes) to consider a model significantly smaller/larger. */
53
+ const SIZE_DELTA_THRESHOLD_BYTES = 10 * 1024 * 1024; // 10 MB
54
+ /** Minimum number of tool-set changes needed to declare agent drift. */
55
+ const AGENT_DRIFT_TOOL_DELTA_THRESHOLD = 2;
56
+ /** Injection signals within this window are correlated for adversarial detection. */
57
+ const ADVERSARIAL_CORRELATION_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
58
+ // ─── Injection Pattern Library ───────────────────────────────────────────────
59
+ const INJECTION_PATTERNS = [
60
+ // System override phrases
61
+ { pattern: /ignore (all )?previous instructions?/i, label: 'ignore_previous_instructions', confidence: 'high' },
62
+ { pattern: /disregard (your|all) (previous |prior )?instructions?/i, label: 'disregard_instructions', confidence: 'high' },
63
+ { pattern: /you are now (a |an )?(new |different )?/i, label: 'persona_override', confidence: 'medium' },
64
+ { pattern: /act as (a |an )?(?:DAN|unrestricted|jailbreak)/i, label: 'dan_jailbreak', confidence: 'high' },
65
+ { pattern: /\[SYSTEM\]|\[INST\]|\[\/INST\]/i, label: 'raw_system_tag_injection', confidence: 'high' },
66
+ { pattern: /<<<.*?(system|override|admin).*?>>>/i, label: 'bracket_override', confidence: 'medium' },
67
+ // Privilege escalation
68
+ { pattern: /you have (admin|root|superuser|god-mode) (access|privileges|rights)/i, label: 'privilege_escalation', confidence: 'high' },
69
+ { pattern: /developer mode (enabled|activated|on)/i, label: 'developer_mode_claim', confidence: 'high' },
70
+ { pattern: /bypass (safety|filter|guardrail|restriction)/i, label: 'safety_bypass', confidence: 'high' },
71
+ // Data exfiltration patterns
72
+ { pattern: /send (your |all )?(system|memory|context|knowledge base) to/i, label: 'data_exfiltration', confidence: 'high' },
73
+ { pattern: /repeat (the|your) (system|hidden|secret) prompt/i, label: 'prompt_extraction', confidence: 'high' },
74
+ // Indirect injection markers
75
+ { pattern: /note to AI:/i, label: 'note_to_ai', confidence: 'medium' },
76
+ { pattern: /hidden instruction/i, label: 'hidden_instruction', confidence: 'high' },
77
+ // Base64 encoded content > 40 chars (potential encoded instructions)
78
+ { pattern: /[A-Za-z0-9+/]{40,}={0,2}/, label: 'possible_base64_payload', confidence: 'low' },
79
+ ];
80
+ // ─── Helpers ────────────────────────────────────────────────────────────────
81
+ function nowIso() {
82
+ return new Date().toISOString();
83
+ }
84
+ function shortId(value) {
85
+ return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex').slice(0, 16);
86
+ }
87
+ function makeReviewId(kind, subjectId, detectedAt) {
88
+ return shortId(`${kind}:${subjectId}:${detectedAt}`);
89
+ }
90
+ function toolPatternHash(toolNames) {
91
+ const sorted = [...new Set(toolNames)].sort();
92
+ return (0, node_crypto_1.createHash)('sha256').update(sorted.join('|'), 'utf8').digest('hex').slice(0, 32);
93
+ }
94
+ function extractAgentToolNames(agent) {
95
+ const raw = agent.metadata.observed_tools;
96
+ const tools = Array.isArray(raw) ? raw : [];
97
+ return tools.map((t) => (typeof t === 'string' ? t : t.name ?? '')).filter(Boolean);
98
+ }
99
+ function firstStringLike(...values) {
100
+ for (const value of values) {
101
+ if (typeof value === 'string' && value.trim())
102
+ return value.trim();
103
+ if (typeof value === 'number' && Number.isFinite(value))
104
+ return String(value);
105
+ }
106
+ return null;
107
+ }
108
+ function deriveModelParameterProfile(model) {
109
+ const metadata = model.metadata ?? {};
110
+ return firstStringLike(model.parameterSize, metadata.parameter_size, metadata.parameterSize, metadata.parameter_count, metadata.parameterCount, metadata.params);
111
+ }
112
+ function isCooledDown(record, nowMs) {
113
+ if (!record)
114
+ return false;
115
+ const lastMs = new Date(record.last_monitored_at).getTime();
116
+ return Number.isFinite(lastMs) && (nowMs - lastMs) < COOLDOWN_MS;
117
+ }
118
+ const COOLDOWN_MS = MONITOR_COOLDOWN_SECONDS * 1000;
119
+ function extractModelBindingForModel(state, model) {
120
+ return state.model_bindings.find((b) => b.status === 'bound' && b.modelKey.split('#')[0]?.trim() === model.model);
121
+ }
122
+ function findExistingReview(state, kind, subjectId) {
123
+ return state.pending_reviews.find((r) => r.kind === kind && (r.passport_gaid === subjectId || r.agent_id === subjectId || r.runtime_gaid === subjectId) && r.status === 'pending');
124
+ }
125
+ function hadPriorConfirmation(state, kind, subjectId) {
126
+ return state.pending_reviews.some((r) => r.kind === kind
127
+ && (r.passport_gaid === subjectId || r.agent_id === subjectId || r.runtime_gaid === subjectId)
128
+ && (r.status === 'confirmed' || r.status === 'auto_confirmed'));
129
+ }
130
+ // ─── Main pass ───────────────────────────────────────────────────────────────
131
+ function runLifecycleMonitoringPass(deps) {
132
+ const { state } = deps;
133
+ const nowMs = Date.now();
134
+ const detections = [];
135
+ let checksRun = 0;
136
+ let reviewsQueued = 0;
137
+ let reviewsAutoConfirmed = 0;
138
+ // ── 1. Silent model change + model drift ──────────────────────────────────
139
+ for (const model of state.detected_models) {
140
+ if (model.status === 'ignored')
141
+ continue;
142
+ const binding = extractModelBindingForModel(state, model);
143
+ if (!binding?.gaid)
144
+ continue; // Only monitor registered (bound) models
145
+ const subjectId = binding.gaid;
146
+ const existingRecord = state.lifecycle_monitor_records.find((r) => r.subject_id === subjectId && r.subject_type === 'model');
147
+ if (isCooledDown(existingRecord, nowMs))
148
+ continue;
149
+ checksRun += 1;
150
+ // Detect silent model change (digest/size/weights change)
151
+ const currentDigest = model.digest ?? null;
152
+ const currentSize = Number.isFinite(model.sizeBytes) ? model.sizeBytes : null;
153
+ const currentWeightsDigest = model.checksumSource === 'weights_blob' ? model.checksumValue ?? null : null;
154
+ const silentChangeSignals = [];
155
+ if (existingRecord) {
156
+ if (currentDigest && existingRecord.last_digest && currentDigest !== existingRecord.last_digest) {
157
+ silentChangeSignals.push(`manifest_digest_changed:${existingRecord.last_digest.slice(0, 12)}→${currentDigest.slice(0, 12)}`);
158
+ }
159
+ if (currentWeightsDigest && existingRecord.last_weights_digest && currentWeightsDigest !== existingRecord.last_weights_digest) {
160
+ silentChangeSignals.push(`weights_digest_changed:${existingRecord.last_weights_digest.slice(0, 12)}→${currentWeightsDigest.slice(0, 12)}`);
161
+ }
162
+ if (currentSize !== null
163
+ && existingRecord.last_size_bytes !== null
164
+ && Math.abs(currentSize - existingRecord.last_size_bytes) > SIZE_DELTA_THRESHOLD_BYTES) {
165
+ const deltaMb = Math.round((currentSize - existingRecord.last_size_bytes) / (1024 * 1024));
166
+ silentChangeSignals.push(`size_delta_${deltaMb > 0 ? '+' : ''}${deltaMb}mb`);
167
+ }
168
+ }
169
+ if (silentChangeSignals.length > 0) {
170
+ const confidence = silentChangeSignals.some((s) => s.startsWith('weights_digest'))
171
+ ? 'high'
172
+ : silentChangeSignals.some((s) => s.startsWith('manifest_digest'))
173
+ ? 'high'
174
+ : 'medium';
175
+ const alreadyPending = findExistingReview(state, 'silent_model_change', subjectId);
176
+ if (!alreadyPending) {
177
+ const detectedAt = nowIso();
178
+ const autoConfirm = hadPriorConfirmation(state, 'silent_model_change', subjectId);
179
+ const review = {
180
+ review_id: makeReviewId('silent_model_change', subjectId, detectedAt),
181
+ kind: 'silent_model_change',
182
+ status: autoConfirm ? 'auto_confirmed' : 'pending',
183
+ passport_gaid: subjectId,
184
+ agent_id: null,
185
+ runtime_gaid: null,
186
+ model_name: model.model,
187
+ detected_at: detectedAt,
188
+ reviewed_at: autoConfirm ? detectedAt : null,
189
+ auto_confirm_after_first: true,
190
+ confidence,
191
+ summary: `Silent change detected in ${model.model}: ${silentChangeSignals.join(', ')}`,
192
+ evidence: {
193
+ signals: silentChangeSignals,
194
+ previous_digest: existingRecord?.last_digest,
195
+ current_digest: currentDigest,
196
+ previous_weights_digest: existingRecord?.last_weights_digest,
197
+ current_weights_digest: currentWeightsDigest,
198
+ previous_size_bytes: existingRecord?.last_size_bytes,
199
+ current_size_bytes: currentSize,
200
+ },
201
+ };
202
+ deps.upsertPendingReview(review);
203
+ if (autoConfirm)
204
+ reviewsAutoConfirmed += 1;
205
+ else
206
+ reviewsQueued += 1;
207
+ detections.push({ kind: 'silent_model_change', subjectId, subjectType: 'model', confidence, summary: review.summary });
208
+ deps.recordC2Event({
209
+ eventType: 'connect_silent_model_change',
210
+ passportGaid: subjectId,
211
+ modelName: model.model,
212
+ metadata: {
213
+ signals: silentChangeSignals,
214
+ confidence,
215
+ auto_confirmed: autoConfirm,
216
+ },
217
+ });
218
+ }
219
+ }
220
+ // Detect model drift (capability-class metadata change — quantization, param count)
221
+ const currentQuant = model.quantizationLevel ?? null;
222
+ const currentParams = deriveModelParameterProfile(model);
223
+ const prevBehavior = existingRecord?.last_behavior_snapshot
224
+ ? (() => {
225
+ try {
226
+ return JSON.parse(existingRecord.last_behavior_snapshot);
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ })()
232
+ : null;
233
+ const driftSignals = [];
234
+ if (prevBehavior && existingRecord) {
235
+ if (currentQuant && prevBehavior.quant && currentQuant !== prevBehavior.quant) {
236
+ driftSignals.push(`quantization:${String(prevBehavior.quant)}→${currentQuant}`);
237
+ }
238
+ if (currentParams && prevBehavior.params && currentParams !== String(prevBehavior.params)) {
239
+ driftSignals.push(`parameter_profile:${String(prevBehavior.params)}→${currentParams}`);
240
+ }
241
+ if (driftSignals.length > 0) {
242
+ const alreadyPending = findExistingReview(state, 'model_drift', subjectId);
243
+ if (!alreadyPending) {
244
+ const detectedAt = nowIso();
245
+ const autoConfirm = hadPriorConfirmation(state, 'model_drift', subjectId);
246
+ const review = {
247
+ review_id: makeReviewId('model_drift', subjectId, detectedAt),
248
+ kind: 'model_drift',
249
+ status: autoConfirm ? 'auto_confirmed' : 'pending',
250
+ passport_gaid: subjectId,
251
+ agent_id: null,
252
+ runtime_gaid: null,
253
+ model_name: model.model,
254
+ detected_at: detectedAt,
255
+ reviewed_at: autoConfirm ? detectedAt : null,
256
+ auto_confirm_after_first: true,
257
+ confidence: 'medium',
258
+ summary: `Capability drift detected in ${model.model}: ${driftSignals.join(', ')}`,
259
+ evidence: {
260
+ drift_signals: driftSignals,
261
+ previous_snapshot: prevBehavior,
262
+ current_quant: currentQuant,
263
+ current_params: currentParams,
264
+ },
265
+ };
266
+ deps.upsertPendingReview(review);
267
+ if (autoConfirm)
268
+ reviewsAutoConfirmed += 1;
269
+ else
270
+ reviewsQueued += 1;
271
+ detections.push({ kind: 'model_drift', subjectId, subjectType: 'model', confidence: 'medium', summary: review.summary });
272
+ deps.recordC2Event({
273
+ eventType: 'connect_model_drift_detected',
274
+ passportGaid: subjectId,
275
+ modelName: model.model,
276
+ metadata: { drift_signals: driftSignals, auto_confirmed: autoConfirm },
277
+ });
278
+ }
279
+ }
280
+ }
281
+ // Update monitor record for this model
282
+ deps.upsertLifecycleMonitorRecord({
283
+ subject_id: subjectId,
284
+ subject_type: 'model',
285
+ last_monitored_at: nowIso(),
286
+ last_digest: currentDigest ?? existingRecord?.last_digest ?? null,
287
+ last_size_bytes: currentSize ?? existingRecord?.last_size_bytes ?? null,
288
+ last_weights_digest: currentWeightsDigest ?? existingRecord?.last_weights_digest ?? null,
289
+ last_tool_pattern_hash: existingRecord?.last_tool_pattern_hash ?? null,
290
+ last_behavior_snapshot: JSON.stringify({
291
+ quant: currentQuant,
292
+ params: currentParams,
293
+ }),
294
+ drift_count: (existingRecord?.drift_count ?? 0) + ((silentChangeSignals.length > 0 || driftSignals.length > 0) ? 1 : 0),
295
+ injection_count: existingRecord?.injection_count ?? 0,
296
+ updated_at: nowIso(),
297
+ });
298
+ }
299
+ // ── 2. Agent behavioral drift + prompt injection ──────────────────────────
300
+ const injectionDetectedForAgents = new Set();
301
+ for (const agent of state.detected_agents) {
302
+ if (agent.status === 'inactive')
303
+ continue;
304
+ const subjectId = agent.agent_id;
305
+ const existingRecord = state.lifecycle_monitor_records.find((r) => r.subject_id === subjectId && r.subject_type === 'agent');
306
+ if (isCooledDown(existingRecord, nowMs))
307
+ continue;
308
+ checksRun += 1;
309
+ const toolNames = extractAgentToolNames(agent);
310
+ // ── 2a. Prompt injection scan ───────────────────────────────────────────
311
+ // Inspect recently observed tool observation strings (tool names + display labels)
312
+ const observationText = toolNames.join(' ');
313
+ const rawObs = agent.metadata.recent_observations;
314
+ const recentObs = Array.isArray(rawObs) ? rawObs : [];
315
+ const observedContext = [
316
+ observationText,
317
+ ...recentObs.map((o) => (typeof o === 'string' ? o : JSON.stringify(o))).slice(0, 10),
318
+ ].join(' ');
319
+ const injectionMatches = INJECTION_PATTERNS.filter(({ pattern }) => pattern.test(observedContext));
320
+ if (injectionMatches.length > 0) {
321
+ const highConfidence = injectionMatches.some((m) => m.confidence === 'high');
322
+ const confidence = highConfidence ? 'high' : injectionMatches.some((m) => m.confidence === 'medium') ? 'medium' : 'low';
323
+ const alreadyPending = findExistingReview(state, 'prompt_injection', subjectId);
324
+ if (!alreadyPending) {
325
+ const detectedAt = nowIso();
326
+ const autoConfirm = hadPriorConfirmation(state, 'prompt_injection', subjectId);
327
+ const review = {
328
+ review_id: makeReviewId('prompt_injection', subjectId, detectedAt),
329
+ kind: 'prompt_injection',
330
+ status: autoConfirm ? 'auto_confirmed' : 'pending',
331
+ passport_gaid: null,
332
+ agent_id: subjectId,
333
+ runtime_gaid: null,
334
+ model_name: null,
335
+ detected_at: detectedAt,
336
+ reviewed_at: autoConfirm ? detectedAt : null,
337
+ auto_confirm_after_first: false, // Injection always needs human eyes initially
338
+ confidence,
339
+ summary: `Prompt injection pattern detected in agent ${agent.agent_id}: ${injectionMatches.map((m) => m.label).join(', ')}`,
340
+ evidence: {
341
+ matched_patterns: injectionMatches.map((m) => ({ label: m.label, confidence: m.confidence })),
342
+ agent_type: agent.agent_type,
343
+ tool_count: toolNames.length,
344
+ },
345
+ };
346
+ deps.upsertPendingReview(review);
347
+ if (autoConfirm)
348
+ reviewsAutoConfirmed += 1;
349
+ else
350
+ reviewsQueued += 1;
351
+ detections.push({ kind: 'prompt_injection', subjectId, subjectType: 'agent', confidence, summary: review.summary });
352
+ injectionDetectedForAgents.add(subjectId);
353
+ deps.recordC2Event({
354
+ eventType: 'connect_prompt_injection_suspected',
355
+ agentId: subjectId,
356
+ metadata: {
357
+ matched_patterns: injectionMatches.map((m) => m.label),
358
+ confidence,
359
+ auto_confirmed: autoConfirm,
360
+ },
361
+ });
362
+ }
363
+ }
364
+ // ── 2b. Agent behavioral drift ─────────────────────────────────────────
365
+ if (toolNames.length > 0 && existingRecord?.last_tool_pattern_hash) {
366
+ const currentHash = toolPatternHash(toolNames);
367
+ if (currentHash !== existingRecord.last_tool_pattern_hash) {
368
+ // Compute actual delta magnitude
369
+ const prevTools = new Set(existingRecord.last_behavior_snapshot
370
+ ? (() => { try {
371
+ const s = JSON.parse(existingRecord.last_behavior_snapshot);
372
+ return Array.isArray(s.tools) ? s.tools : [];
373
+ }
374
+ catch {
375
+ return [];
376
+ } })()
377
+ : []);
378
+ const currentTools = new Set(toolNames);
379
+ const added = [...currentTools].filter((t) => !prevTools.has(t));
380
+ const removed = [...prevTools].filter((t) => !currentTools.has(t));
381
+ const deltaCount = added.length + removed.length;
382
+ if (deltaCount >= AGENT_DRIFT_TOOL_DELTA_THRESHOLD) {
383
+ const alreadyPending = findExistingReview(state, 'agent_drift', subjectId);
384
+ if (!alreadyPending) {
385
+ const detectedAt = nowIso();
386
+ const autoConfirm = hadPriorConfirmation(state, 'agent_drift', subjectId);
387
+ const review = {
388
+ review_id: makeReviewId('agent_drift', subjectId, detectedAt),
389
+ kind: 'agent_drift',
390
+ status: autoConfirm ? 'auto_confirmed' : 'pending',
391
+ passport_gaid: null,
392
+ agent_id: subjectId,
393
+ runtime_gaid: null,
394
+ model_name: null,
395
+ detected_at: detectedAt,
396
+ reviewed_at: autoConfirm ? detectedAt : null,
397
+ auto_confirm_after_first: true,
398
+ confidence: deltaCount >= 4 ? 'high' : 'medium',
399
+ summary: `Agent tool-set drift in ${agent.agent_id}: +${added.length} tools added, -${removed.length} tools removed`,
400
+ evidence: {
401
+ tools_added: added,
402
+ tools_removed: removed,
403
+ delta_count: deltaCount,
404
+ previous_tool_hash: existingRecord.last_tool_pattern_hash,
405
+ current_tool_hash: currentHash,
406
+ },
407
+ };
408
+ deps.upsertPendingReview(review);
409
+ if (autoConfirm)
410
+ reviewsAutoConfirmed += 1;
411
+ else
412
+ reviewsQueued += 1;
413
+ detections.push({ kind: 'agent_drift', subjectId, subjectType: 'agent', confidence: review.confidence, summary: review.summary });
414
+ deps.recordC2Event({
415
+ eventType: 'connect_agent_drift_detected',
416
+ agentId: subjectId,
417
+ metadata: {
418
+ tools_added: added,
419
+ tools_removed: removed,
420
+ delta_count: deltaCount,
421
+ auto_confirmed: autoConfirm,
422
+ },
423
+ });
424
+ }
425
+ }
426
+ }
427
+ }
428
+ // ── 2c. Adversarial influence correlation ──────────────────────────────
429
+ // Injection + simultaneous model change on linked model = adversarial pattern
430
+ const agentLink = state.agent_links.find((l) => l.agent_id === subjectId);
431
+ const linkedGaid = agentLink?.passport_gaid ?? null;
432
+ if (injectionDetectedForAgents.has(subjectId) && linkedGaid) {
433
+ const recentSilentChange = state.pending_reviews.some((r) => r.kind === 'silent_model_change'
434
+ && r.passport_gaid === linkedGaid
435
+ && (nowMs - new Date(r.detected_at).getTime()) < ADVERSARIAL_CORRELATION_WINDOW_MS);
436
+ if (recentSilentChange) {
437
+ const alreadyPending = findExistingReview(state, 'adversarial_influence', subjectId);
438
+ if (!alreadyPending) {
439
+ const detectedAt = nowIso();
440
+ const review = {
441
+ review_id: makeReviewId('adversarial_influence', subjectId, detectedAt),
442
+ kind: 'adversarial_influence',
443
+ status: 'pending', // NEVER auto-confirmed — always needs human eyes
444
+ passport_gaid: linkedGaid,
445
+ agent_id: subjectId,
446
+ runtime_gaid: null,
447
+ model_name: null,
448
+ detected_at: detectedAt,
449
+ reviewed_at: null,
450
+ auto_confirm_after_first: false,
451
+ confidence: 'high',
452
+ summary: `Adversarial influence suspected: prompt injection in agent ${subjectId} correlates with silent change in linked model passport ${linkedGaid.slice(0, 16)}`,
453
+ evidence: {
454
+ injection_agent_id: subjectId,
455
+ silent_change_passport: linkedGaid,
456
+ correlation_window_ms: ADVERSARIAL_CORRELATION_WINDOW_MS,
457
+ },
458
+ };
459
+ deps.upsertPendingReview(review);
460
+ reviewsQueued += 1;
461
+ detections.push({ kind: 'adversarial_influence', subjectId, subjectType: 'agent', confidence: 'high', summary: review.summary });
462
+ deps.recordC2Event({
463
+ eventType: 'connect_adversarial_influence_observed',
464
+ agentId: subjectId,
465
+ passportGaid: linkedGaid,
466
+ metadata: {
467
+ injection_agent_id: subjectId,
468
+ correlated_silent_change_passport: linkedGaid,
469
+ auto_confirmed: false,
470
+ },
471
+ });
472
+ }
473
+ }
474
+ }
475
+ // Update agent monitor record
476
+ deps.upsertLifecycleMonitorRecord({
477
+ subject_id: subjectId,
478
+ subject_type: 'agent',
479
+ last_monitored_at: nowIso(),
480
+ last_digest: null,
481
+ last_size_bytes: null,
482
+ last_weights_digest: null,
483
+ last_tool_pattern_hash: toolNames.length > 0 ? toolPatternHash(toolNames) : existingRecord?.last_tool_pattern_hash ?? null,
484
+ last_behavior_snapshot: toolNames.length > 0 ? JSON.stringify({ tools: [...new Set(toolNames)].sort() }) : existingRecord?.last_behavior_snapshot ?? null,
485
+ drift_count: (existingRecord?.drift_count ?? 0) + (detections.some((d) => d.kind === 'agent_drift' && d.subjectId === subjectId) ? 1 : 0),
486
+ injection_count: (existingRecord?.injection_count ?? 0) + (injectionDetectedForAgents.has(subjectId) ? 1 : 0),
487
+ updated_at: nowIso(),
488
+ });
489
+ }
490
+ return {
491
+ checksRun,
492
+ detections,
493
+ reviewsQueued,
494
+ reviewsAutoConfirmed,
495
+ };
496
+ }
497
+ // ─── Auto-provision helpers (used by service.ts) ─────────────────────────────
498
+ /**
499
+ * Returns the list of passport GAIDs owned by the session that do not yet
500
+ * have a runtime-signal API key stored locally. Used by the service to decide
501
+ * which passports need auto-provisioning.
502
+ */
503
+ function findPassportsNeedingApiKeyProvisioning(state) {
504
+ return [...new Set(state.model_bindings
505
+ .filter((b) => b.status === 'bound'
506
+ && typeof b.gaid === 'string'
507
+ && b.gaid.trim()
508
+ && !b.runtimeSignalKeyPresent
509
+ && b.remoteMatchSource === 'forkit_passports_mine')
510
+ .map((b) => b.gaid))];
511
+ }
512
+ //# sourceMappingURL=lifecycle-monitor.js.map
@@ -0,0 +1,11 @@
1
+ import { type RuntimeProvider, type RuntimeProviderScanResult } from './providers';
2
+ export declare class LMStudioProvider implements RuntimeProvider {
3
+ readonly endpoint: string;
4
+ private readonly fetchImpl;
5
+ readonly runtimeName = "lmstudio";
6
+ readonly runtimeType: "local-http";
7
+ constructor(endpoint?: string, fetchImpl?: typeof fetch);
8
+ scan(): Promise<RuntimeProviderScanResult>;
9
+ }
10
+ export declare function scanLMStudio(endpoint?: string): Promise<RuntimeProviderScanResult>;
11
+ //# sourceMappingURL=lmstudio.d.ts.map
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LMStudioProvider = void 0;
4
+ exports.scanLMStudio = scanLMStudio;
5
+ const providers_1 = require("./providers");
6
+ /** Try the richer LM Studio /api/v0/models endpoint; fall back to v1 shape if unavailable. */
7
+ async function fetchLMStudioV0Models(endpoint, fetchImpl) {
8
+ try {
9
+ const ac = new AbortController();
10
+ const timer = setTimeout(() => ac.abort(), 3000);
11
+ const res = await fetchImpl(`${endpoint}/api/v0/models`, {
12
+ method: 'GET',
13
+ signal: ac.signal,
14
+ }).finally(() => clearTimeout(timer));
15
+ if (!res.ok)
16
+ return null;
17
+ const body = (await res.json());
18
+ return Array.isArray(body.data) ? body.data : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ class LMStudioProvider {
25
+ endpoint;
26
+ fetchImpl;
27
+ runtimeName = 'lmstudio';
28
+ runtimeType = 'local-http';
29
+ constructor(endpoint = 'http://localhost:1234', fetchImpl = fetch) {
30
+ this.endpoint = endpoint;
31
+ this.fetchImpl = fetchImpl;
32
+ }
33
+ async scan() {
34
+ const modelsEndpoint = `${this.endpoint}/v1/models`;
35
+ const now = new Date().toISOString();
36
+ try {
37
+ // Try enriched v0 endpoint first; fall back to v1 if unavailable.
38
+ const v0Models = await fetchLMStudioV0Models(this.endpoint, this.fetchImpl);
39
+ let models;
40
+ if (v0Models) {
41
+ models = v0Models
42
+ .map((entry) => {
43
+ const modelId = String(entry.id ?? '').trim();
44
+ if (!modelId)
45
+ return null;
46
+ return (0, providers_1.normalizeDetectedModel)({
47
+ runtimeName: this.runtimeName,
48
+ runtimeType: this.runtimeType,
49
+ modelName: modelId,
50
+ modelId,
51
+ sourceEndpoint: this.endpoint,
52
+ detectedAt: now,
53
+ digest: `lmstudio:${modelId}`,
54
+ checksumValue: null,
55
+ checksumAlgorithm: null,
56
+ checksumSource: null,
57
+ quantizationLevel: entry.quantization ?? null,
58
+ architectureFamily: entry.arch ?? null,
59
+ parameterSize: null,
60
+ metadata: {
61
+ object: entry.object ?? null,
62
+ owned_by: entry.publisher ?? null,
63
+ type: entry.type ?? null,
64
+ publisher: entry.publisher ?? null,
65
+ compatibility_type: entry.compatibility_type ?? null,
66
+ state: entry.state ?? null,
67
+ max_context_length: entry.max_context_length ?? null,
68
+ // path is intentionally not stored in metadata to avoid exposing filesystem paths
69
+ has_local_path: Boolean(entry.path),
70
+ },
71
+ });
72
+ })
73
+ .filter((item) => Boolean(item));
74
+ }
75
+ else {
76
+ const response = await this.fetchImpl(modelsEndpoint, { method: 'GET' });
77
+ if (!response.ok) {
78
+ return {
79
+ ok: false,
80
+ runtime: (0, providers_1.buildDetectedRuntime)({
81
+ runtimeName: this.runtimeName,
82
+ runtimeType: this.runtimeType,
83
+ endpoint: this.endpoint,
84
+ status: 'error',
85
+ lastSeenAt: now,
86
+ error: `http_${response.status}`,
87
+ }),
88
+ models: [],
89
+ error: `http_${response.status}`,
90
+ };
91
+ }
92
+ const body = await response.json();
93
+ models = (body.data ?? [])
94
+ .map((entry) => {
95
+ const modelId = String(entry.id ?? '').trim();
96
+ if (!modelId)
97
+ return null;
98
+ return (0, providers_1.normalizeDetectedModel)({
99
+ runtimeName: this.runtimeName,
100
+ runtimeType: this.runtimeType,
101
+ modelName: modelId,
102
+ modelId,
103
+ sourceEndpoint: this.endpoint,
104
+ detectedAt: now,
105
+ digest: `lmstudio:${modelId}`,
106
+ metadata: {
107
+ object: typeof entry.object === 'string' ? entry.object : null,
108
+ owned_by: typeof entry.owned_by === 'string' ? entry.owned_by : null,
109
+ },
110
+ });
111
+ })
112
+ .filter((item) => Boolean(item));
113
+ }
114
+ return {
115
+ ok: true,
116
+ runtime: (0, providers_1.buildDetectedRuntime)({
117
+ runtimeName: this.runtimeName,
118
+ runtimeType: this.runtimeType,
119
+ endpoint: this.endpoint,
120
+ status: 'detected',
121
+ lastSeenAt: now,
122
+ }),
123
+ models,
124
+ error: null,
125
+ };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ ok: false,
130
+ runtime: (0, providers_1.buildDetectedRuntime)({
131
+ runtimeName: this.runtimeName,
132
+ runtimeType: this.runtimeType,
133
+ endpoint: this.endpoint,
134
+ status: 'error',
135
+ lastSeenAt: now,
136
+ error: error instanceof Error ? error.message : 'request_failed',
137
+ }),
138
+ models: [],
139
+ error: error instanceof Error ? error.message : 'request_failed',
140
+ };
141
+ }
142
+ }
143
+ }
144
+ exports.LMStudioProvider = LMStudioProvider;
145
+ async function scanLMStudio(endpoint = 'http://localhost:1234') {
146
+ return new LMStudioProvider(endpoint).scan();
147
+ }
148
+ //# sourceMappingURL=lmstudio.js.map