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,463 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readBoundModelName = readBoundModelName;
|
|
4
|
+
exports.listBoundBindings = listBoundBindings;
|
|
5
|
+
exports.selectBoundBinding = selectBoundBinding;
|
|
6
|
+
exports.sendAllBoundHeartbeats = sendAllBoundHeartbeats;
|
|
7
|
+
exports.sendBoundHeartbeat = sendBoundHeartbeat;
|
|
8
|
+
const api_1 = require("./api");
|
|
9
|
+
const discovery_1 = require("./discovery");
|
|
10
|
+
const DEFAULT_BASE_URL = process.env.FORKIT_API_URL ?? process.env.FORKIT_BASE_URL ?? 'https://www.forkit.dev';
|
|
11
|
+
function isDeploymentCheckinResponse(body) {
|
|
12
|
+
return !!body && typeof body === 'object';
|
|
13
|
+
}
|
|
14
|
+
function readBackendCode(body) {
|
|
15
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
16
|
+
return null;
|
|
17
|
+
const code = body.code;
|
|
18
|
+
return typeof code === 'string' && code.trim() ? code : null;
|
|
19
|
+
}
|
|
20
|
+
function isConnectBindingFailureCode(code) {
|
|
21
|
+
return typeof code === 'string' && code.startsWith('CONNECT_BINDING_');
|
|
22
|
+
}
|
|
23
|
+
function buildRuntimeSyncBindingFailure(service, backendCode, status, details) {
|
|
24
|
+
const runtimeSyncStatus = service.getBindingActionStatus('runtime_session_sync');
|
|
25
|
+
switch (runtimeSyncStatus.reasonCode) {
|
|
26
|
+
case 'binding_revoked':
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
code: 'credential_reconnect_needed',
|
|
30
|
+
message: 'Device revoked. Reconnect Forkit Connect to continue.',
|
|
31
|
+
status,
|
|
32
|
+
details,
|
|
33
|
+
};
|
|
34
|
+
case 'binding_paused':
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
code: 'binding_paused',
|
|
38
|
+
message: 'Sync paused. Local observations continue, but runtime sync is held.',
|
|
39
|
+
status,
|
|
40
|
+
details,
|
|
41
|
+
};
|
|
42
|
+
case 'binding_stale':
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
code: 'binding_stale',
|
|
46
|
+
message: 'Connection stale. Runtime sync is held until Connect refreshes the binding.',
|
|
47
|
+
status,
|
|
48
|
+
details,
|
|
49
|
+
};
|
|
50
|
+
case 'binding_reconsent_required':
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
code: 'binding_reconsent_required',
|
|
54
|
+
message: 'Consent update required before runtime sync can continue.',
|
|
55
|
+
status,
|
|
56
|
+
details,
|
|
57
|
+
};
|
|
58
|
+
case 'binding_missing':
|
|
59
|
+
case 'binding_scope_required':
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
code: 'missing_binding',
|
|
63
|
+
message: runtimeSyncStatus.message,
|
|
64
|
+
status,
|
|
65
|
+
details,
|
|
66
|
+
};
|
|
67
|
+
default:
|
|
68
|
+
if (backendCode === 'CONNECT_BINDING_REVOKED') {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
code: 'credential_reconnect_needed',
|
|
72
|
+
message: 'Device revoked. Reconnect Forkit Connect to continue.',
|
|
73
|
+
status,
|
|
74
|
+
details,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (backendCode === 'CONNECT_BINDING_PAUSED') {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
code: 'binding_paused',
|
|
81
|
+
message: 'Sync paused. Local observations continue, but runtime sync is held.',
|
|
82
|
+
status,
|
|
83
|
+
details,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (backendCode === 'CONNECT_BINDING_STALE' || backendCode === 'CONNECT_BINDING_NOT_ACTIVE') {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
code: 'binding_stale',
|
|
90
|
+
message: 'Connection stale. Runtime sync is held until Connect refreshes the binding.',
|
|
91
|
+
status,
|
|
92
|
+
details,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (backendCode === 'CONNECT_BINDING_RECONSENT_REQUIRED' || backendCode === 'CONNECT_BINDING_CONSENT_REQUIRED') {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
code: 'binding_reconsent_required',
|
|
99
|
+
message: 'Consent update required before runtime sync can continue.',
|
|
100
|
+
status,
|
|
101
|
+
details,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
code: 'missing_binding',
|
|
107
|
+
message: 'Runtime sync is waiting for an active Forkit.dev workspace and project binding.',
|
|
108
|
+
status,
|
|
109
|
+
details,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function normalizeHeartbeatSessionMode(runtimeMode, revokedAt) {
|
|
114
|
+
if (revokedAt)
|
|
115
|
+
return 'revoked';
|
|
116
|
+
switch (String(runtimeMode || '').trim().toLowerCase()) {
|
|
117
|
+
case 'paused':
|
|
118
|
+
return 'paused';
|
|
119
|
+
case 'standby':
|
|
120
|
+
case 'draining':
|
|
121
|
+
return 'standby';
|
|
122
|
+
case 'sleeping':
|
|
123
|
+
return 'sleeping';
|
|
124
|
+
case 'offline':
|
|
125
|
+
return 'offline';
|
|
126
|
+
case 'revoked':
|
|
127
|
+
return 'revoked';
|
|
128
|
+
default:
|
|
129
|
+
return 'active';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function readBoundModelName(modelKey) {
|
|
133
|
+
const [name] = String(modelKey || '').split('#');
|
|
134
|
+
return name?.trim() || 'unknown';
|
|
135
|
+
}
|
|
136
|
+
function listBoundBindings(state) {
|
|
137
|
+
if (!Array.isArray(state.model_bindings)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return state.model_bindings
|
|
141
|
+
.filter((item) => item.status === 'bound' && typeof item.gaid === 'string' && item.gaid.trim())
|
|
142
|
+
.sort((left, right) => String(right.updatedAt || '').localeCompare(String(left.updatedAt || '')))
|
|
143
|
+
.map((binding) => ({
|
|
144
|
+
binding,
|
|
145
|
+
detectedModel: state.detected_models.find((item) => `${item.model}#${item.digest}` === binding.modelKey),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
function selectBoundBinding(state) {
|
|
149
|
+
return listBoundBindings(state)[0] ?? null;
|
|
150
|
+
}
|
|
151
|
+
function buildSessionId(state, gaid) {
|
|
152
|
+
return `connect:${state.runtime_identity.runtimeId}:${String(gaid).slice(0, 32)}`;
|
|
153
|
+
}
|
|
154
|
+
async function sendHeartbeatForSelection(service, selected, options) {
|
|
155
|
+
const state = service.getStateStore().readState();
|
|
156
|
+
const storedSessionRef = String(service.readSessionRef() || '').trim();
|
|
157
|
+
const effectiveBinding = service.getEffectiveBinding();
|
|
158
|
+
const runtimeScope = service.getActionScope('runtime_session_sync', state);
|
|
159
|
+
const runtimeSyncStatus = service.getBindingActionStatus('runtime_session_sync');
|
|
160
|
+
if (!storedSessionRef) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
code: 'not_authenticated',
|
|
164
|
+
message: 'Not authenticated. Run forkit-connect login first.',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (!runtimeSyncStatus.allowed) {
|
|
168
|
+
if (runtimeSyncStatus.reasonCode === 'binding_revoked') {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
code: 'credential_reconnect_needed',
|
|
172
|
+
message: runtimeSyncStatus.message,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (runtimeSyncStatus.reasonCode === 'binding_paused') {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: 'binding_paused',
|
|
179
|
+
message: runtimeSyncStatus.message,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (runtimeSyncStatus.reasonCode === 'binding_stale') {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
code: 'binding_stale',
|
|
186
|
+
message: runtimeSyncStatus.message,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (runtimeSyncStatus.reasonCode === 'binding_reconsent_required') {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
code: 'binding_reconsent_required',
|
|
193
|
+
message: runtimeSyncStatus.message,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
code: 'missing_binding',
|
|
199
|
+
message: runtimeSyncStatus.message,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const boundWorkspaceId = String(runtimeScope?.workspaceId || '').trim();
|
|
203
|
+
const boundProjectId = String(runtimeScope?.projectId || '').trim();
|
|
204
|
+
if (!boundWorkspaceId || !boundProjectId) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
code: 'missing_binding',
|
|
208
|
+
message: 'No active workspace/project binding is available for runtime sync.',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!selected.binding.gaid) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
code: 'missing_model',
|
|
215
|
+
message: 'No bound model passport found. Run forkit-connect register/publish first.',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const runtimeName = selected.detectedModel?.runtime || 'ollama';
|
|
219
|
+
const runtime = state.detected_runtimes.find((item) => (item.runtime === runtimeName
|
|
220
|
+
&& (!selected.binding.runtimeDiscoveryHash || item.discoveryHash === selected.binding.runtimeDiscoveryHash))) ?? state.detected_runtimes.find((item) => item.runtime === runtimeName);
|
|
221
|
+
const modelName = selected.detectedModel?.model || readBoundModelName(selected.binding.modelKey);
|
|
222
|
+
const sessionId = buildSessionId(state, selected.binding.gaid);
|
|
223
|
+
const api = new api_1.ConnectApiClient({
|
|
224
|
+
baseUrl: options?.baseUrl ?? DEFAULT_BASE_URL,
|
|
225
|
+
sessionRef: storedSessionRef,
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
const heartbeatResult = await api.sendDeploymentCheckin(selected.binding.gaid, {
|
|
229
|
+
binding_id: runtimeScope?.bindingId ?? null,
|
|
230
|
+
sessionId,
|
|
231
|
+
sessionLabel: `Forkit Connect ${modelName}`,
|
|
232
|
+
environment: 'local',
|
|
233
|
+
hostLabel: state.runtime_identity.hostname,
|
|
234
|
+
runtimeLabel: 'Forkit Connect',
|
|
235
|
+
runtimeMode: 'active',
|
|
236
|
+
resourceAccuracy: 'unknown',
|
|
237
|
+
metadata: {
|
|
238
|
+
provider: runtimeName,
|
|
239
|
+
runtime: runtimeName,
|
|
240
|
+
source: 'Verified by Runtime',
|
|
241
|
+
sourceLabel: 'Verified by Runtime',
|
|
242
|
+
workspaceId: boundWorkspaceId,
|
|
243
|
+
projectId: boundProjectId,
|
|
244
|
+
workspace_id: boundWorkspaceId,
|
|
245
|
+
project_id: boundProjectId,
|
|
246
|
+
binding_id: runtimeScope?.bindingId ?? null,
|
|
247
|
+
connect_device_id: runtimeScope?.connectDeviceId ?? effectiveBinding?.binding.connectDeviceId ?? null,
|
|
248
|
+
evidence_type: 'observed_runtime_session',
|
|
249
|
+
model: modelName,
|
|
250
|
+
digest: selected.detectedModel?.digest ?? null,
|
|
251
|
+
endpoint_hash: runtime?.endpoint ? (0, discovery_1.buildEndpointHash)(runtime.endpoint) : null,
|
|
252
|
+
source_endpoint_label: runtime?.endpoint ? (0, discovery_1.buildSourceEndpointLabel)(runtime.endpoint) : null,
|
|
253
|
+
runtimeId: state.runtime_identity.runtimeId,
|
|
254
|
+
hostname: state.runtime_identity.hostname,
|
|
255
|
+
platform: state.runtime_identity.platform,
|
|
256
|
+
arch: state.runtime_identity.arch,
|
|
257
|
+
originTool: 'forkit-connect',
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
if (!heartbeatResult.ok) {
|
|
261
|
+
const backendCode = readBackendCode(heartbeatResult.body);
|
|
262
|
+
if (isConnectBindingFailureCode(backendCode)) {
|
|
263
|
+
try {
|
|
264
|
+
await service.refreshEffectiveBinding();
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// The backend failure is still authoritative enough to surface to the caller.
|
|
268
|
+
}
|
|
269
|
+
return buildRuntimeSyncBindingFailure(service, backendCode, heartbeatResult.status, heartbeatResult.body);
|
|
270
|
+
}
|
|
271
|
+
const observedSession = service.observeBackendCommunicationState({
|
|
272
|
+
passportGaid: selected.binding.gaid,
|
|
273
|
+
runtimeGaid: selected.binding.runtimeGaid ?? null,
|
|
274
|
+
modelName,
|
|
275
|
+
sessionId: selected.binding.activeSessionId ?? sessionId,
|
|
276
|
+
status: heartbeatResult.status,
|
|
277
|
+
body: heartbeatResult.body,
|
|
278
|
+
source: 'heartbeat',
|
|
279
|
+
});
|
|
280
|
+
if (observedSession?.local_sync_state === 'credential_reconnect_needed') {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
code: 'credential_reconnect_needed',
|
|
284
|
+
message: 'Reconnect Forkit Connect to continue governed communication.',
|
|
285
|
+
status: heartbeatResult.status,
|
|
286
|
+
details: heartbeatResult.body,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (observedSession?.session_mode === 'paused') {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
code: 'governor_paused',
|
|
293
|
+
message: 'Passport communication is currently paused by Forkit governance.',
|
|
294
|
+
status: heartbeatResult.status,
|
|
295
|
+
details: heartbeatResult.body,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (observedSession?.session_mode === 'revoked') {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
code: 'access_revoked',
|
|
302
|
+
message: 'Passport communication access has been revoked by Forkit governance.',
|
|
303
|
+
status: heartbeatResult.status,
|
|
304
|
+
details: heartbeatResult.body,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (heartbeatResult.status === 403 || heartbeatResult.status === 404) {
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
code: 'passport_inaccessible',
|
|
311
|
+
message: 'Bound passport not found or not accessible.',
|
|
312
|
+
status: heartbeatResult.status,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (heartbeatResult.status >= 500) {
|
|
316
|
+
service.observeTransientRuntimeSyncFailure({
|
|
317
|
+
passportGaid: selected.binding.gaid,
|
|
318
|
+
runtimeGaid: selected.binding.runtimeGaid ?? null,
|
|
319
|
+
modelName,
|
|
320
|
+
sessionId: selected.binding.activeSessionId ?? sessionId,
|
|
321
|
+
source: 'heartbeat',
|
|
322
|
+
status: heartbeatResult.status,
|
|
323
|
+
reason: `heartbeat_${heartbeatResult.status}`,
|
|
324
|
+
details: heartbeatResult.body,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
code: 'heartbeat_failed',
|
|
330
|
+
message: `[forkit-connect] Heartbeat failed (${heartbeatResult.status}).`,
|
|
331
|
+
status: heartbeatResult.status,
|
|
332
|
+
details: heartbeatResult.body,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (!isDeploymentCheckinResponse(heartbeatResult.body) || !heartbeatResult.body.session) {
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
code: 'heartbeat_failed',
|
|
339
|
+
message: `[forkit-connect] Heartbeat failed (${heartbeatResult.status}).`,
|
|
340
|
+
status: heartbeatResult.status,
|
|
341
|
+
details: heartbeatResult.body,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
service.getStateStore().addEvidenceEvent({
|
|
345
|
+
type: 'runtime_heartbeat_sent',
|
|
346
|
+
details: {
|
|
347
|
+
gaid: selected.binding.gaid,
|
|
348
|
+
model: modelName,
|
|
349
|
+
workspaceId: boundWorkspaceId,
|
|
350
|
+
projectId: boundProjectId,
|
|
351
|
+
runtime: runtimeName,
|
|
352
|
+
provider: runtimeName,
|
|
353
|
+
source: 'Verified by Runtime',
|
|
354
|
+
sessionId: heartbeatResult.body.session.sessionId ?? sessionId,
|
|
355
|
+
sessionLabel: heartbeatResult.body.session.sessionLabel ?? `Forkit Connect ${modelName}`,
|
|
356
|
+
runtimeMode: heartbeatResult.body.session.runtimeMode ?? 'active',
|
|
357
|
+
lastCheckinAt: heartbeatResult.body.session.lastCheckinAt ?? null,
|
|
358
|
+
backendStatus: heartbeatResult.status,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
service.observeDeploymentSession({
|
|
362
|
+
sessionId: heartbeatResult.body.session.sessionId ?? sessionId,
|
|
363
|
+
passportGaid: selected.binding.gaid,
|
|
364
|
+
runtimeGaid: selected.binding.runtimeGaid ?? null,
|
|
365
|
+
modelName,
|
|
366
|
+
workspaceId: boundWorkspaceId,
|
|
367
|
+
projectId: boundProjectId,
|
|
368
|
+
sessionMode: normalizeHeartbeatSessionMode(heartbeatResult.body.session.runtimeMode ?? 'active', heartbeatResult.body.session.revokedAt ?? null),
|
|
369
|
+
controlSource: 'website',
|
|
370
|
+
lastSeenAt: heartbeatResult.body.session.lastCheckinAt ?? null,
|
|
371
|
+
lastControlSeenAt: heartbeatResult.body.session.revokedAt ?? heartbeatResult.body.session.lastCheckinAt ?? null,
|
|
372
|
+
metadata: {
|
|
373
|
+
session_label: heartbeatResult.body.session.sessionLabel ?? `Forkit Connect ${modelName}`,
|
|
374
|
+
runtime_mode: heartbeatResult.body.session.runtimeMode ?? 'active',
|
|
375
|
+
resource_accuracy: heartbeatResult.body.session.resourceAccuracy ?? null,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
ok: true,
|
|
380
|
+
gaid: selected.binding.gaid,
|
|
381
|
+
modelName,
|
|
382
|
+
sessionId: heartbeatResult.body.session.sessionId ?? sessionId,
|
|
383
|
+
lastCheckinAt: heartbeatResult.body.session.lastCheckinAt ?? null,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
service.observeTransientRuntimeSyncFailure({
|
|
388
|
+
passportGaid: selected.binding.gaid,
|
|
389
|
+
runtimeGaid: selected.binding.runtimeGaid ?? null,
|
|
390
|
+
modelName,
|
|
391
|
+
sessionId: selected.binding.activeSessionId ?? sessionId,
|
|
392
|
+
source: 'heartbeat',
|
|
393
|
+
reason: error instanceof Error ? error.message : 'heartbeat_transport_failed',
|
|
394
|
+
details: error instanceof Error ? error.message : error,
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
code: 'heartbeat_failed',
|
|
399
|
+
message: '[forkit-connect] Heartbeat failed (transport).',
|
|
400
|
+
details: error instanceof Error ? error.message : error,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function sendAllBoundHeartbeats(service, options) {
|
|
405
|
+
const selections = listBoundBindings(service.getStateStore().readState());
|
|
406
|
+
const results = [];
|
|
407
|
+
let attempted = 0;
|
|
408
|
+
let succeeded = 0;
|
|
409
|
+
let failed = 0;
|
|
410
|
+
let runtimeSignalQueued = 0;
|
|
411
|
+
for (const selection of selections) {
|
|
412
|
+
if (!selection.binding.gaid) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
attempted += 1;
|
|
416
|
+
const result = await sendHeartbeatForSelection(service, selection, options);
|
|
417
|
+
const runtimeSignalQueueId = result.ok && options?.queueRuntimeSignal !== false
|
|
418
|
+
? service.queueConfiguredHeartbeatRuntimeSignal(result.gaid)
|
|
419
|
+
: null;
|
|
420
|
+
if (result.ok) {
|
|
421
|
+
succeeded += 1;
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
failed += 1;
|
|
425
|
+
}
|
|
426
|
+
if (runtimeSignalQueueId) {
|
|
427
|
+
runtimeSignalQueued += 1;
|
|
428
|
+
}
|
|
429
|
+
results.push({
|
|
430
|
+
gaid: selection.binding.gaid,
|
|
431
|
+
modelName: selection.detectedModel?.model || readBoundModelName(selection.binding.modelKey),
|
|
432
|
+
runtimeSignalQueueId,
|
|
433
|
+
result,
|
|
434
|
+
});
|
|
435
|
+
if (!result.ok && (result.code === 'not_authenticated'
|
|
436
|
+
|| result.code === 'missing_binding'
|
|
437
|
+
|| result.code === 'credential_reconnect_needed'
|
|
438
|
+
|| result.code === 'binding_paused'
|
|
439
|
+
|| result.code === 'binding_stale'
|
|
440
|
+
|| result.code === 'binding_reconsent_required')) {
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
attempted,
|
|
446
|
+
succeeded,
|
|
447
|
+
failed,
|
|
448
|
+
runtimeSignalQueued,
|
|
449
|
+
results,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async function sendBoundHeartbeat(service, options) {
|
|
453
|
+
const selected = selectBoundBinding(service.getStateStore().readState());
|
|
454
|
+
if (!selected) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
code: 'missing_model',
|
|
458
|
+
message: 'No bound model passport found. Run forkit-connect register/publish first.',
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return sendHeartbeatForSelection(service, selected, options);
|
|
462
|
+
}
|
|
463
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifecycle-monitor.ts
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* Deep AI model and agent lifecycle observability engine for Forkit Connect.
|
|
5
|
+
*
|
|
6
|
+
* Capabilities (fully automatic, 24/7, login-only onboarding):
|
|
7
|
+
*
|
|
8
|
+
* 1. Silent model change detection
|
|
9
|
+
* Tracks digest, size, and weights-layer hash per model binding.
|
|
10
|
+
* Emits `connect_silent_model_change` when any of these change without an
|
|
11
|
+
* announced version event, queuing a PendingReview for human acknowledgement.
|
|
12
|
+
*
|
|
13
|
+
* 2. Model drift detection (metadata-based)
|
|
14
|
+
* Tracks quantization label and parameter count changes over scan cycles.
|
|
15
|
+
* Emits `connect_model_drift_detected` on capability-class changes.
|
|
16
|
+
*
|
|
17
|
+
* 3. Agent behavioral drift detection
|
|
18
|
+
* Computes a hash of each agent's distinct observed tool set. If the tool
|
|
19
|
+
* fingerprint changes significantly (tools added or removed) it emits
|
|
20
|
+
* `connect_agent_drift_detected`.
|
|
21
|
+
*
|
|
22
|
+
* 4. Prompt injection detection
|
|
23
|
+
* Scans agent tool-call observations for known injection patterns:
|
|
24
|
+
* system-override phrases, base64-encoded instruction payloads, DAN-style
|
|
25
|
+
* prompts. Emits `connect_prompt_injection_suspected` with confidence level.
|
|
26
|
+
*
|
|
27
|
+
* 5. Adversarial influence detection
|
|
28
|
+
* Correlates: (a) multiple injection signals within the same scan window,
|
|
29
|
+
* (b) a prompt injection AND a silent model change on the same agent/model
|
|
30
|
+
* pair. When correlated, emits `connect_adversarial_influence_observed`.
|
|
31
|
+
*
|
|
32
|
+
* Human-in-the-loop flow:
|
|
33
|
+
* Every detection writes a PendingReview. Humans can confirm/dismiss via
|
|
34
|
+
* `c2 reviews` CLI commands. Once a pattern is confirmed once, subsequent
|
|
35
|
+
* detections of the same kind for the same subject are auto-confirmed,
|
|
36
|
+
* giving full automation with initial oversight.
|
|
37
|
+
*
|
|
38
|
+
* Privacy contract:
|
|
39
|
+
* This module never reads prompt content or model outputs directly.
|
|
40
|
+
* It only inspects: model metadata (name, digest, size, quantization label,
|
|
41
|
+
* parameter count), agent tool names (not arguments), and detection signals
|
|
42
|
+
* already recorded in the evidence chain.
|
|
43
|
+
*/
|
|
44
|
+
import type { ConnectState, C2LiteEventType, LifecycleMonitorRecord, PendingReview, PendingReviewKind } from './types';
|
|
45
|
+
export interface LifecycleMonitorDependencies {
|
|
46
|
+
state: ConnectState;
|
|
47
|
+
upsertPendingReview: (review: PendingReview) => void;
|
|
48
|
+
upsertLifecycleMonitorRecord: (record: LifecycleMonitorRecord) => void;
|
|
49
|
+
recordC2Event: (input: {
|
|
50
|
+
eventType: C2LiteEventType;
|
|
51
|
+
passportGaid?: string | null;
|
|
52
|
+
agentId?: string | null;
|
|
53
|
+
runtimeGaid?: string | null;
|
|
54
|
+
modelName?: string | null;
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
56
|
+
}) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface MonitorPassResult {
|
|
59
|
+
checksRun: number;
|
|
60
|
+
detections: MonitorDetection[];
|
|
61
|
+
reviewsQueued: number;
|
|
62
|
+
reviewsAutoConfirmed: number;
|
|
63
|
+
}
|
|
64
|
+
export interface MonitorDetection {
|
|
65
|
+
kind: PendingReviewKind;
|
|
66
|
+
subjectId: string;
|
|
67
|
+
subjectType: 'model' | 'agent' | 'runtime';
|
|
68
|
+
confidence: 'high' | 'medium' | 'low';
|
|
69
|
+
summary: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function runLifecycleMonitoringPass(deps: LifecycleMonitorDependencies): MonitorPassResult;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the list of passport GAIDs owned by the session that do not yet
|
|
74
|
+
* have a runtime-signal API key stored locally. Used by the service to decide
|
|
75
|
+
* which passports need auto-provisioning.
|
|
76
|
+
*/
|
|
77
|
+
export declare function findPassportsNeedingApiKeyProvisioning(state: ConnectState): string[];
|
|
78
|
+
//# sourceMappingURL=lifecycle-monitor.d.ts.map
|