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.
- package/QUICKSTART.md +55 -0
- package/README.md +96 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +4724 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -0
- package/dist/launcher.d.ts +33 -0
- package/dist/launcher.js +9344 -0
- package/dist/ps-list-loader.d.ts +5 -0
- package/dist/ps-list-loader.js +20 -0
- package/dist/v1/agent-observation.d.ts +42 -0
- package/dist/v1/agent-observation.js +499 -0
- package/dist/v1/api.d.ts +276 -0
- package/dist/v1/api.js +390 -0
- package/dist/v1/credential-store.d.ts +92 -0
- package/dist/v1/credential-store.js +797 -0
- package/dist/v1/currency.d.ts +41 -0
- package/dist/v1/currency.js +127 -0
- package/dist/v1/daemon.d.ts +50 -0
- package/dist/v1/daemon.js +265 -0
- package/dist/v1/discovery.d.ts +61 -0
- package/dist/v1/discovery.js +168 -0
- package/dist/v1/filesystem-models.d.ts +11 -0
- package/dist/v1/filesystem-models.js +261 -0
- package/dist/v1/heartbeat.d.ts +45 -0
- package/dist/v1/heartbeat.js +463 -0
- package/dist/v1/lifecycle-monitor.d.ts +78 -0
- package/dist/v1/lifecycle-monitor.js +512 -0
- package/dist/v1/lmstudio.d.ts +11 -0
- package/dist/v1/lmstudio.js +148 -0
- package/dist/v1/ollama.d.ts +19 -0
- package/dist/v1/ollama.js +164 -0
- package/dist/v1/openai-compatible.d.ts +12 -0
- package/dist/v1/openai-compatible.js +124 -0
- package/dist/v1/process-scout.d.ts +50 -0
- package/dist/v1/process-scout.js +715 -0
- package/dist/v1/providers.d.ts +50 -0
- package/dist/v1/providers.js +106 -0
- package/dist/v1/service.d.ts +680 -0
- package/dist/v1/service.js +8286 -0
- package/dist/v1/state.d.ts +87 -0
- package/dist/v1/state.js +1318 -0
- package/dist/v1/test-credential-backend.d.ts +19 -0
- package/dist/v1/test-credential-backend.js +49 -0
- package/dist/v1/types.d.ts +873 -0
- package/dist/v1/types.js +3 -0
- package/dist/v1/update.d.ts +38 -0
- package/dist/v1/update.js +184 -0
- package/dist/v1/vitality-pulse.d.ts +36 -0
- package/dist/v1/vitality-pulse.js +512 -0
- 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
|