forkit-connect 0.1.0 → 0.1.1
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/README.md +32 -4
- package/dist/cli.js +310 -66
- package/dist/v1/api.d.ts +62 -1
- package/dist/v1/api.js +131 -2
- package/dist/v1/credential-store.d.ts +1 -0
- package/dist/v1/credential-store.js +15 -3
- package/dist/v1/service.d.ts +55 -3
- package/dist/v1/service.js +605 -49
- package/dist/v1/state.d.ts +1 -0
- package/dist/v1/state.js +32 -12
- package/dist/v1/types.d.ts +55 -1
- package/package.json +3 -3
package/dist/v1/service.js
CHANGED
|
@@ -50,6 +50,7 @@ const SMART_INBOX_CONFIDENCE_PRIORITY = {
|
|
|
50
50
|
medium: 1,
|
|
51
51
|
low: 2,
|
|
52
52
|
};
|
|
53
|
+
const SMART_INBOX_FRESH_MAX_AGE_MS = 45 * 1000;
|
|
53
54
|
function isRecord(value) {
|
|
54
55
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
55
56
|
}
|
|
@@ -568,6 +569,7 @@ function isGovernorRevokedStatus(status, code) {
|
|
|
568
569
|
class ConnectV1Service {
|
|
569
570
|
stateStore;
|
|
570
571
|
credentialStore;
|
|
572
|
+
smartInboxBackgroundRefreshInFlight = false;
|
|
571
573
|
constructor(stateDir, options) {
|
|
572
574
|
this.stateStore = new state_1.LocalStateStore(stateDir);
|
|
573
575
|
this.stateStore.ensureInitialized();
|
|
@@ -580,6 +582,68 @@ class ConnectV1Service {
|
|
|
580
582
|
getStateStore() {
|
|
581
583
|
return this.stateStore;
|
|
582
584
|
}
|
|
585
|
+
cloneInboxSnapshot(inbox) {
|
|
586
|
+
return JSON.parse(JSON.stringify(inbox));
|
|
587
|
+
}
|
|
588
|
+
withInboxFreshness(inbox, freshness, ageMs) {
|
|
589
|
+
const next = this.cloneInboxSnapshot(inbox);
|
|
590
|
+
next.summary = {
|
|
591
|
+
...next.summary,
|
|
592
|
+
freshness_state: freshness,
|
|
593
|
+
snapshot_age_seconds: Math.max(0, Math.floor(ageMs / 1000)),
|
|
594
|
+
};
|
|
595
|
+
return next;
|
|
596
|
+
}
|
|
597
|
+
persistSmartInboxSnapshot(inbox) {
|
|
598
|
+
const state = this.stateStore.readState();
|
|
599
|
+
state.smart_inbox_snapshot = this.cloneInboxSnapshot(inbox);
|
|
600
|
+
state.smart_inbox_snapshot_updated_at = nowIso();
|
|
601
|
+
this.stateStore.writeState(state);
|
|
602
|
+
}
|
|
603
|
+
getSmartRegistrationInbox(options) {
|
|
604
|
+
const preferSnapshot = options?.preferSnapshot !== false;
|
|
605
|
+
const forceRefresh = options?.forceRefresh === true;
|
|
606
|
+
const maxSnapshotAgeMs = Math.max(1000, options?.maxSnapshotAgeMs ?? SMART_INBOX_FRESH_MAX_AGE_MS);
|
|
607
|
+
const refreshInBackground = options?.refreshInBackground !== false;
|
|
608
|
+
const state = this.stateStore.readState();
|
|
609
|
+
const snapshot = state.smart_inbox_snapshot;
|
|
610
|
+
const updatedAtMs = state.smart_inbox_snapshot_updated_at ? Date.parse(state.smart_inbox_snapshot_updated_at) : Number.NaN;
|
|
611
|
+
const ageMs = Number.isFinite(updatedAtMs) ? Math.max(0, Date.now() - updatedAtMs) : Number.MAX_SAFE_INTEGER;
|
|
612
|
+
if (!forceRefresh && preferSnapshot && snapshot) {
|
|
613
|
+
if (this.smartInboxBackgroundRefreshInFlight) {
|
|
614
|
+
return this.withInboxFreshness(snapshot, 'syncing', ageMs);
|
|
615
|
+
}
|
|
616
|
+
if (ageMs <= maxSnapshotAgeMs) {
|
|
617
|
+
return this.withInboxFreshness(snapshot, 'fresh', ageMs);
|
|
618
|
+
}
|
|
619
|
+
if (refreshInBackground) {
|
|
620
|
+
this.refreshSmartRegistrationInboxInBackground();
|
|
621
|
+
}
|
|
622
|
+
return this.withInboxFreshness(snapshot, refreshInBackground ? 'syncing' : 'stale', ageMs);
|
|
623
|
+
}
|
|
624
|
+
const liveInbox = this.buildSmartRegistrationInbox();
|
|
625
|
+
this.persistSmartInboxSnapshot(liveInbox);
|
|
626
|
+
return this.withInboxFreshness(liveInbox, 'fresh', 0);
|
|
627
|
+
}
|
|
628
|
+
refreshSmartRegistrationInboxInBackground() {
|
|
629
|
+
if (this.smartInboxBackgroundRefreshInFlight) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
this.smartInboxBackgroundRefreshInFlight = true;
|
|
633
|
+
void Promise.resolve().then(() => {
|
|
634
|
+
try {
|
|
635
|
+
const liveInbox = this.buildSmartRegistrationInbox();
|
|
636
|
+
this.persistSmartInboxSnapshot(liveInbox);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
this.smartInboxBackgroundRefreshInFlight = false;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
async prewarmSmartRegistrationInbox() {
|
|
644
|
+
const liveInbox = this.buildSmartRegistrationInbox();
|
|
645
|
+
this.persistSmartInboxSnapshot(liveInbox);
|
|
646
|
+
}
|
|
583
647
|
getStoredSessionRef() {
|
|
584
648
|
return this.credentialStore.getSessionRef();
|
|
585
649
|
}
|
|
@@ -1547,6 +1611,9 @@ class ConnectV1Service {
|
|
|
1547
1611
|
backendBaseUrl: DEFAULT_BASE_URL,
|
|
1548
1612
|
};
|
|
1549
1613
|
}
|
|
1614
|
+
getCredentialStoreStatus() {
|
|
1615
|
+
return this.credentialStore.getStatus();
|
|
1616
|
+
}
|
|
1550
1617
|
getConfig() {
|
|
1551
1618
|
return this.stateStore.readState().connect_config;
|
|
1552
1619
|
}
|
|
@@ -1691,6 +1758,16 @@ class ConnectV1Service {
|
|
|
1691
1758
|
// Preserve the current item failure and let the next pass reconcile binding state again.
|
|
1692
1759
|
}
|
|
1693
1760
|
}
|
|
1761
|
+
if (input.source === 'runtimeSignal' && input.passportGaid && isCredentialInvalidStatus(input.status, code)) {
|
|
1762
|
+
this.clearRuntimeSignalApiKeyForGaid(input.passportGaid, 'recovery');
|
|
1763
|
+
try {
|
|
1764
|
+
await this.autoProvisionRuntimeSignalKeys({ suppressErrors: true });
|
|
1765
|
+
}
|
|
1766
|
+
catch {
|
|
1767
|
+
// Keep the failure visible in local state. A later cycle or manual key
|
|
1768
|
+
// configuration can recover if provisioning is still blocked.
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1694
1771
|
const observedSession = this.observeBackendCommunicationState(input);
|
|
1695
1772
|
return {
|
|
1696
1773
|
observedSession,
|
|
@@ -1946,10 +2023,18 @@ class ConnectV1Service {
|
|
|
1946
2023
|
if (!sessionRef) {
|
|
1947
2024
|
return this.getC2SessionSummary();
|
|
1948
2025
|
}
|
|
2026
|
+
const workspaceId = String(state.workspace_binding.workspaceId || '').trim();
|
|
2027
|
+
const projectId = String(state.project_binding.projectId || '').trim();
|
|
2028
|
+
const scope = workspaceId || projectId
|
|
2029
|
+
? {
|
|
2030
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
2031
|
+
...(projectId ? { projectId } : {}),
|
|
2032
|
+
}
|
|
2033
|
+
: undefined;
|
|
1949
2034
|
const boundGaids = [...new Set(state.model_bindings.filter((binding) => binding.status !== 'ignored' && typeof binding.gaid === 'string' && binding.gaid?.trim()).map((binding) => binding.gaid))];
|
|
1950
2035
|
for (const passportGaid of boundGaids) {
|
|
1951
2036
|
try {
|
|
1952
|
-
const result = await this.getApiClient(state).getDeployments(passportGaid);
|
|
2037
|
+
const result = await this.getApiClient(state).getDeployments(passportGaid, scope);
|
|
1953
2038
|
if (!result.ok) {
|
|
1954
2039
|
this.observeBackendCommunicationState({
|
|
1955
2040
|
passportGaid,
|
|
@@ -2104,6 +2189,41 @@ class ConnectV1Service {
|
|
|
2104
2189
|
}
|
|
2105
2190
|
return null;
|
|
2106
2191
|
}
|
|
2192
|
+
hasExactBoundPassportForModel(state, model) {
|
|
2193
|
+
const modelName = String(model.model || '').trim().toLowerCase();
|
|
2194
|
+
const digest = String(model.digest || '').trim();
|
|
2195
|
+
if (!modelName || !digest) {
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
return state.model_bindings.some((binding) => {
|
|
2199
|
+
if (binding.status !== 'bound') {
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2202
|
+
const boundGaid = String(binding.gaid || '').trim();
|
|
2203
|
+
if (!boundGaid) {
|
|
2204
|
+
return false;
|
|
2205
|
+
}
|
|
2206
|
+
const bindingModelName = modelNameFromKey(binding.modelKey).trim().toLowerCase();
|
|
2207
|
+
const bindingDigest = String(modelDigestFromKey(binding.modelKey) || '').trim();
|
|
2208
|
+
return bindingModelName === modelName && bindingDigest === digest;
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
runtimeHasExactBoundModelDuplicate(state, runtimePassport) {
|
|
2212
|
+
for (const model of state.detected_models) {
|
|
2213
|
+
if (this.getModelReviewDisposition(model) !== 'active') {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
const detectedRuntime = this.findDetectedRuntimeForModel(state, model);
|
|
2217
|
+
const candidateRuntimePassport = detectedRuntime ? this.findRuntimePassportForRuntime(state, detectedRuntime) : null;
|
|
2218
|
+
if (!candidateRuntimePassport || candidateRuntimePassport.runtime_gaid !== runtimePassport.runtime_gaid) {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
if (this.hasExactBoundPassportForModel(state, model)) {
|
|
2222
|
+
return true;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2107
2227
|
clearModelReviewDeferral(model) {
|
|
2108
2228
|
if (model.review_state !== 'deferred' && !model.review_deferred_until) {
|
|
2109
2229
|
return;
|
|
@@ -2544,6 +2664,87 @@ class ConnectV1Service {
|
|
|
2544
2664
|
return undefined;
|
|
2545
2665
|
return state.model_bindings.find((binding) => binding.gaid === gaid);
|
|
2546
2666
|
}
|
|
2667
|
+
findTrainingAnchorBinding(state, input) {
|
|
2668
|
+
const normalizedModelName = input.modelName.trim().toLowerCase();
|
|
2669
|
+
if (!normalizedModelName)
|
|
2670
|
+
return undefined;
|
|
2671
|
+
const normalizedWorkspaceId = String(input.workspaceId || '').trim() || null;
|
|
2672
|
+
const normalizedProjectId = String(input.projectId || '').trim() || null;
|
|
2673
|
+
const candidates = state.model_bindings.filter((binding) => {
|
|
2674
|
+
if (!binding.gaid || binding.status === 'ignored')
|
|
2675
|
+
return false;
|
|
2676
|
+
return modelNameFromKey(binding.modelKey).trim().toLowerCase() === normalizedModelName;
|
|
2677
|
+
});
|
|
2678
|
+
if (candidates.length === 0)
|
|
2679
|
+
return undefined;
|
|
2680
|
+
return [...candidates].sort((left, right) => {
|
|
2681
|
+
const leftProjectMatch = normalizedProjectId && String(left.projectId || '').trim() === normalizedProjectId;
|
|
2682
|
+
const rightProjectMatch = normalizedProjectId && String(right.projectId || '').trim() === normalizedProjectId;
|
|
2683
|
+
if (leftProjectMatch !== rightProjectMatch)
|
|
2684
|
+
return leftProjectMatch ? -1 : 1;
|
|
2685
|
+
const leftWorkspaceMatch = normalizedWorkspaceId && String(left.workspaceId || '').trim() === normalizedWorkspaceId;
|
|
2686
|
+
const rightWorkspaceMatch = normalizedWorkspaceId && String(right.workspaceId || '').trim() === normalizedWorkspaceId;
|
|
2687
|
+
if (leftWorkspaceMatch !== rightWorkspaceMatch)
|
|
2688
|
+
return leftWorkspaceMatch ? -1 : 1;
|
|
2689
|
+
const leftBound = left.status === 'bound';
|
|
2690
|
+
const rightBound = right.status === 'bound';
|
|
2691
|
+
if (leftBound !== rightBound)
|
|
2692
|
+
return leftBound ? -1 : 1;
|
|
2693
|
+
return String(right.updatedAt || '').localeCompare(String(left.updatedAt || ''));
|
|
2694
|
+
})[0];
|
|
2695
|
+
}
|
|
2696
|
+
anchorBuildSessionToBinding(state, session) {
|
|
2697
|
+
if (session.passport_gaid)
|
|
2698
|
+
return session;
|
|
2699
|
+
const binding = this.findTrainingAnchorBinding(state, {
|
|
2700
|
+
modelName: session.model_name,
|
|
2701
|
+
workspaceId: session.workspaceId,
|
|
2702
|
+
projectId: session.projectId,
|
|
2703
|
+
});
|
|
2704
|
+
if (!binding?.gaid)
|
|
2705
|
+
return session;
|
|
2706
|
+
return {
|
|
2707
|
+
...session,
|
|
2708
|
+
passport_gaid: binding.gaid,
|
|
2709
|
+
workspaceId: session.workspaceId || binding.workspaceId || null,
|
|
2710
|
+
projectId: session.projectId || binding.projectId || null,
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
resolveRuntimeRunScope(state, passportGaid) {
|
|
2714
|
+
const trimScope = (value) => String(value || '').trim() || null;
|
|
2715
|
+
const binding = this.findBindingByGaid(state, passportGaid);
|
|
2716
|
+
if (binding) {
|
|
2717
|
+
const primaryBound = this.getPrimaryBoundBinding(state);
|
|
2718
|
+
return {
|
|
2719
|
+
workspaceId: trimScope(binding.workspaceId) ?? (primaryBound?.gaid === binding.gaid ? trimScope(state.workspace_binding.workspaceId) : null),
|
|
2720
|
+
projectId: trimScope(binding.projectId) ?? (primaryBound?.gaid === binding.gaid ? trimScope(state.project_binding.projectId) : null),
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
const latestSession = [...state.c2_sessions]
|
|
2724
|
+
.filter((session) => session.passport_gaid === passportGaid)
|
|
2725
|
+
.sort((left, right) => String(right.last_seen_at || right.last_control_seen_at || '').localeCompare(String(left.last_seen_at || left.last_control_seen_at || '')))
|
|
2726
|
+
.at(0);
|
|
2727
|
+
if (latestSession) {
|
|
2728
|
+
return {
|
|
2729
|
+
workspaceId: trimScope(latestSession.workspaceId),
|
|
2730
|
+
projectId: trimScope(latestSession.projectId),
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
const runtimeGaid = state.agent_links.find((link) => link.passport_gaid === passportGaid)?.runtime_gaid ?? null;
|
|
2734
|
+
if (runtimeGaid) {
|
|
2735
|
+
const runtimePassport = state.runtime_passports.find((runtime) => runtime.runtime_gaid === runtimeGaid);
|
|
2736
|
+
if (runtimePassport) {
|
|
2737
|
+
return {
|
|
2738
|
+
workspaceId: trimScope(runtimePassport.workspaceId),
|
|
2739
|
+
projectId: trimScope(runtimePassport.projectId),
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
workspaceId: null,
|
|
2745
|
+
projectId: null,
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2547
2748
|
getPrimaryBoundBinding(state) {
|
|
2548
2749
|
return state.model_bindings.find((binding) => binding.status === 'bound' && typeof binding.gaid === 'string' && binding.gaid.trim());
|
|
2549
2750
|
}
|
|
@@ -2562,6 +2763,28 @@ class ConnectV1Service {
|
|
|
2562
2763
|
updatedAt: nowIso(),
|
|
2563
2764
|
});
|
|
2564
2765
|
}
|
|
2766
|
+
clearRuntimeSignalApiKeyForGaid(gaid, source = 'reset') {
|
|
2767
|
+
if (!gaid)
|
|
2768
|
+
return;
|
|
2769
|
+
const state = this.stateStore.readState();
|
|
2770
|
+
const binding = this.findBindingByGaid(state, gaid);
|
|
2771
|
+
this.credentialStore.clearRuntimeSignalApiKey(gaid);
|
|
2772
|
+
if (!binding)
|
|
2773
|
+
return;
|
|
2774
|
+
this.stateStore.upsertModelBinding({
|
|
2775
|
+
...binding,
|
|
2776
|
+
runtimeSignalKeyPresent: false,
|
|
2777
|
+
runtimeSignalKeyRef: null,
|
|
2778
|
+
updatedAt: nowIso(),
|
|
2779
|
+
});
|
|
2780
|
+
this.stateStore.addEvidenceEvent({
|
|
2781
|
+
type: 'credential_invalid_observed',
|
|
2782
|
+
details: {
|
|
2783
|
+
source,
|
|
2784
|
+
gaid,
|
|
2785
|
+
},
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2565
2788
|
resolveRuntimeSignalApiKeyForEvent(state, passportGaid) {
|
|
2566
2789
|
const binding = this.findBindingByGaid(state, passportGaid);
|
|
2567
2790
|
return String(this.credentialStore.getRuntimeSignalApiKey(passportGaid)
|
|
@@ -2834,6 +3057,65 @@ class ConnectV1Service {
|
|
|
2834
3057
|
boundGaid: boundBinding?.gaid ?? null,
|
|
2835
3058
|
};
|
|
2836
3059
|
}
|
|
3060
|
+
async emitRuntimeRunLog(input) {
|
|
3061
|
+
const state = this.stateStore.readState();
|
|
3062
|
+
const gaid = String(input.gaid || '').trim();
|
|
3063
|
+
const apiKey = String(input.apiKey || this.resolveRuntimeSignalApiKeyForEvent(state, gaid) || '').trim();
|
|
3064
|
+
if (!gaid || !apiKey) {
|
|
3065
|
+
return {
|
|
3066
|
+
ok: false,
|
|
3067
|
+
status: 0,
|
|
3068
|
+
body: {
|
|
3069
|
+
error: 'Runtime signal API key required. Run `forkit-connect c2 set-key --heartbeat-gaid <gaid> --heartbeat-key <key>` or pass --api-key from a secure environment.',
|
|
3070
|
+
code: 'MISSING_RUNTIME_SIGNAL_API_KEY',
|
|
3071
|
+
},
|
|
3072
|
+
contract: null,
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
const startedAt = input.startedAt || nowIso();
|
|
3076
|
+
const endedAt = input.endedAt || (input.status && input.status !== 'running' ? startedAt : undefined);
|
|
3077
|
+
const runId = String(input.runId || `forkit-connect-${(0, node_crypto_1.randomUUID)()}`).trim();
|
|
3078
|
+
const promptTokens = Math.max(0, Math.trunc(input.promptTokens ?? 0));
|
|
3079
|
+
const completionTokens = Math.max(0, Math.trunc(input.completionTokens ?? 0));
|
|
3080
|
+
const totalTokens = promptTokens + completionTokens;
|
|
3081
|
+
const { workspaceId, projectId } = this.resolveRuntimeRunScope(state, gaid);
|
|
3082
|
+
return this.getApiClient(state).pushRuntimeRunLog({
|
|
3083
|
+
gaid,
|
|
3084
|
+
apiKey,
|
|
3085
|
+
schemaVersion: 'runtime.run.v1',
|
|
3086
|
+
run: {
|
|
3087
|
+
runId,
|
|
3088
|
+
passportGaid: gaid,
|
|
3089
|
+
provider: String(input.provider || 'custom').trim() || 'custom',
|
|
3090
|
+
model: String(input.model || 'unknown').trim() || 'unknown',
|
|
3091
|
+
serviceName: String(input.serviceName || 'Forkit Connect CLI').trim() || 'Forkit Connect CLI',
|
|
3092
|
+
serviceKind: String(input.serviceKind || 'custom').trim() || 'custom',
|
|
3093
|
+
...(input.externalRunId ? { externalRunId: String(input.externalRunId).trim() } : {}),
|
|
3094
|
+
workspaceId,
|
|
3095
|
+
projectId,
|
|
3096
|
+
status: String(input.status || 'completed').trim() || 'completed',
|
|
3097
|
+
startedAt,
|
|
3098
|
+
...(endedAt ? { endedAt } : {}),
|
|
3099
|
+
usage: {
|
|
3100
|
+
promptTokens,
|
|
3101
|
+
completionTokens,
|
|
3102
|
+
...(input.cachedPromptTokens !== undefined ? { cachedPromptTokens: Math.max(0, Math.trunc(input.cachedPromptTokens)) } : {}),
|
|
3103
|
+
...(input.reasoningTokens !== undefined ? { reasoningTokens: Math.max(0, Math.trunc(input.reasoningTokens)) } : {}),
|
|
3104
|
+
totalTokens,
|
|
3105
|
+
...(input.latencyMs !== undefined ? { latencyMs: Math.max(0, Math.trunc(input.latencyMs)) } : {}),
|
|
3106
|
+
...(input.estimatedCostCents !== undefined ? { estimatedCostCents: Math.max(0, Number(input.estimatedCostCents)) } : {}),
|
|
3107
|
+
currency: String(input.currency || 'USD').trim().toUpperCase().slice(0, 3) || 'USD',
|
|
3108
|
+
},
|
|
3109
|
+
...(input.summary ? { summary: String(input.summary).trim().slice(0, 4000) } : {}),
|
|
3110
|
+
metadata: {
|
|
3111
|
+
source: 'forkit-connect-cli',
|
|
3112
|
+
safe_metadata_only: true,
|
|
3113
|
+
connect_device_id: state.connect_identity.connect_device_id,
|
|
3114
|
+
},
|
|
3115
|
+
},
|
|
3116
|
+
steps: [],
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
2837
3119
|
findAgentByIdOrName(state, selector) {
|
|
2838
3120
|
const normalized = selector.trim().toLowerCase();
|
|
2839
3121
|
const exactId = state.detected_agents.find((agent) => agent.agent_id === selector || agent.agent_id.startsWith(selector));
|
|
@@ -4083,10 +4365,12 @@ class ConnectV1Service {
|
|
|
4083
4365
|
continue;
|
|
4084
4366
|
}
|
|
4085
4367
|
const connectableModel = this.findConnectableModelForRuntime(finalState, runtimePassport);
|
|
4086
|
-
const
|
|
4368
|
+
const runtimeHasExactDuplicateBoundModel = this.runtimeHasExactBoundModelDuplicate(finalState, runtimePassport);
|
|
4369
|
+
const runtimeIsOnlyDuplicatingModelRegistration = Boolean(!connectableModel
|
|
4087
4370
|
&& runtimePassport.status !== 'unavailable'
|
|
4088
4371
|
&& runtimePassport.linked_model_gaids.length === 0
|
|
4089
|
-
&& runtimePassport.linked_agent_ids.length === 0
|
|
4372
|
+
&& runtimePassport.linked_agent_ids.length === 0
|
|
4373
|
+
&& runtimeHasExactDuplicateBoundModel);
|
|
4090
4374
|
if (runtimeIsOnlyDuplicatingModelRegistration) {
|
|
4091
4375
|
continue;
|
|
4092
4376
|
}
|
|
@@ -4240,13 +4524,16 @@ class ConnectV1Service {
|
|
|
4240
4524
|
website_is_governance_authority: true,
|
|
4241
4525
|
};
|
|
4242
4526
|
}
|
|
4243
|
-
getConnectStatusOverview() {
|
|
4527
|
+
getConnectStatusOverview(options) {
|
|
4244
4528
|
const state = this.stateStore.readState();
|
|
4245
|
-
const
|
|
4529
|
+
const includeInbox = options?.includeInbox !== false;
|
|
4530
|
+
const inbox = includeInbox ? this.buildSmartRegistrationInbox() : null;
|
|
4246
4531
|
const sessionSummary = this.getC2SessionSummary();
|
|
4247
4532
|
const binding = this.getEffectiveBindingState(state);
|
|
4248
4533
|
const bindingReconnectRequired = this.bindingRequiresReconnect(state);
|
|
4249
4534
|
const eventSummary = this.getC2EventSyncSummary(state);
|
|
4535
|
+
const discoveredModels = state.detected_models.filter((item) => item.status !== 'ignored').length;
|
|
4536
|
+
const connectedCount = state.model_bindings.filter((bindingItem) => bindingItem.status === 'bound').length;
|
|
4250
4537
|
return {
|
|
4251
4538
|
device_paired: state.connect_identity.connect_identity_status === 'paired',
|
|
4252
4539
|
workspace_id: state.workspace_binding.workspaceId,
|
|
@@ -4254,18 +4541,18 @@ class ConnectV1Service {
|
|
|
4254
4541
|
binding_state: binding?.binding.state ?? null,
|
|
4255
4542
|
lifecycle_note: this.getBindingLifecycleNotice(state),
|
|
4256
4543
|
daemon_status: this.getDaemonStatus().daemon_running ? 'running' : 'stopped',
|
|
4257
|
-
models_discovered:
|
|
4544
|
+
models_discovered: discoveredModels,
|
|
4258
4545
|
agents_discovered: state.detected_agents.length,
|
|
4259
4546
|
runtimes_discovered: state.runtime_passports.length,
|
|
4260
4547
|
paused_session_count: sessionSummary.paused_session_count,
|
|
4261
4548
|
revoked_session_count: sessionSummary.revoked_session_count,
|
|
4262
4549
|
credential_reconnect_needed: bindingReconnectRequired || sessionSummary.credential_reconnect_needed,
|
|
4263
|
-
ready_to_connect_count: inbox
|
|
4264
|
-
needs_confirmation_count: inbox
|
|
4265
|
-
connected_count: inbox
|
|
4550
|
+
ready_to_connect_count: inbox?.summary.ready_to_connect_count ?? discoveredModels,
|
|
4551
|
+
needs_confirmation_count: inbox?.summary.needs_confirmation_count ?? 0,
|
|
4552
|
+
connected_count: inbox?.summary.connected_count ?? connectedCount,
|
|
4266
4553
|
c2_sync_pending: eventSummary.pendingSyncable,
|
|
4267
4554
|
privacy_mode: 'metadata only',
|
|
4268
|
-
next_recommended_action: inbox
|
|
4555
|
+
next_recommended_action: inbox?.summary.next_recommended_action ?? null,
|
|
4269
4556
|
};
|
|
4270
4557
|
}
|
|
4271
4558
|
resolveRecommendedNextAction(state) {
|
|
@@ -4529,6 +4816,11 @@ class ConnectV1Service {
|
|
|
4529
4816
|
const inbox = this.buildSmartRegistrationInbox();
|
|
4530
4817
|
const state = this.stateStore.readState();
|
|
4531
4818
|
const candidates = [];
|
|
4819
|
+
const inboxActionableCounts = {
|
|
4820
|
+
inbox_ready_to_connect_count: inbox.summary.ready_to_connect_count,
|
|
4821
|
+
inbox_needs_confirmation_count: inbox.summary.needs_confirmation_count,
|
|
4822
|
+
inbox_actionable_count: inbox.summary.ready_to_connect_count + inbox.summary.needs_confirmation_count,
|
|
4823
|
+
};
|
|
4532
4824
|
const pulseSummary = state.last_pulse_summary;
|
|
4533
4825
|
const sessionSummary = this.getC2SessionSummary();
|
|
4534
4826
|
const pendingDraftEvent = [...state.evidence_events]
|
|
@@ -4583,6 +4875,7 @@ class ConnectV1Service {
|
|
|
4583
4875
|
suggested_action: 'connect_model',
|
|
4584
4876
|
severity: scopeMismatchDetected ? 'red' : 'amber',
|
|
4585
4877
|
metadata: {
|
|
4878
|
+
...inboxActionableCounts,
|
|
4586
4879
|
item_id: entry.item_id,
|
|
4587
4880
|
model_name: entry.display_name,
|
|
4588
4881
|
runtime_gaid: entry.runtime_gaid,
|
|
@@ -4641,6 +4934,7 @@ class ConnectV1Service {
|
|
|
4641
4934
|
suggested_action: 'review_agent',
|
|
4642
4935
|
severity: scopeMismatchDetected ? 'red' : 'amber',
|
|
4643
4936
|
metadata: {
|
|
4937
|
+
...inboxActionableCounts,
|
|
4644
4938
|
item_id: entry.item_id,
|
|
4645
4939
|
agent_id: entry.agent_id,
|
|
4646
4940
|
display_name: entry.display_name,
|
|
@@ -4675,6 +4969,7 @@ class ConnectV1Service {
|
|
|
4675
4969
|
suggested_action: 'open_inbox',
|
|
4676
4970
|
severity: 'green',
|
|
4677
4971
|
metadata: {
|
|
4972
|
+
...inboxActionableCounts,
|
|
4678
4973
|
item_id: entry.item_id,
|
|
4679
4974
|
matched_passport_gaid: entry.matched_passport_gaid,
|
|
4680
4975
|
match_reason: entry.match_reason,
|
|
@@ -4705,6 +5000,7 @@ class ConnectV1Service {
|
|
|
4705
5000
|
suggested_action: 'open_inbox',
|
|
4706
5001
|
severity: 'amber',
|
|
4707
5002
|
metadata: {
|
|
5003
|
+
...inboxActionableCounts,
|
|
4708
5004
|
item_id: entry.item_id,
|
|
4709
5005
|
display_name: entry.display_name,
|
|
4710
5006
|
relationship_summary: 'Existing Passport metadata changed',
|
|
@@ -4810,6 +5106,7 @@ class ConnectV1Service {
|
|
|
4810
5106
|
suggested_action: 'open_inbox',
|
|
4811
5107
|
severity: 'amber',
|
|
4812
5108
|
metadata: {
|
|
5109
|
+
...inboxActionableCounts,
|
|
4813
5110
|
process_name: finding.process_name,
|
|
4814
5111
|
reason: finding.reason,
|
|
4815
5112
|
relationship_summary: 'System-detected runtime candidate',
|
|
@@ -4837,9 +5134,14 @@ class ConnectV1Service {
|
|
|
4837
5134
|
suggested_action: 'view_pulse_history',
|
|
4838
5135
|
severity: 'red',
|
|
4839
5136
|
metadata: {
|
|
5137
|
+
runtime_gaid: entry.runtime_gaid ?? null,
|
|
4840
5138
|
runtime_name: entry.runtime_name,
|
|
4841
5139
|
model_name: entry.model_name,
|
|
4842
5140
|
pulse_status: entry.pulse_status,
|
|
5141
|
+
bound_passport_gaid: entry.bound_passport_gaid ?? null,
|
|
5142
|
+
binding_status: entry.binding_status ?? null,
|
|
5143
|
+
registration_state: entry.registration_state ?? null,
|
|
5144
|
+
connection_classification: entry.connection_classification ?? null,
|
|
4843
5145
|
relationship_summary: 'Existing runtime availability changed',
|
|
4844
5146
|
source_summary: sourceSummary,
|
|
4845
5147
|
scope_summary: null,
|
|
@@ -4900,8 +5202,203 @@ class ConnectV1Service {
|
|
|
4900
5202
|
}
|
|
4901
5203
|
return candidates;
|
|
4902
5204
|
}
|
|
5205
|
+
getNotificationMetadataNumber(candidate, key) {
|
|
5206
|
+
const value = candidate.metadata[key];
|
|
5207
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
5208
|
+
}
|
|
5209
|
+
getMaxNotificationMetadataNumber(candidates, key) {
|
|
5210
|
+
return candidates.reduce((max, candidate) => Math.max(max, this.getNotificationMetadataNumber(candidate, key)), 0);
|
|
5211
|
+
}
|
|
5212
|
+
pluralizeNotificationNoun(count, singular, plural = `${singular}s`) {
|
|
5213
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
5214
|
+
}
|
|
5215
|
+
buildConnectInboxReviewNotification(candidates) {
|
|
5216
|
+
const reviewCandidates = candidates.filter((candidate) => (candidate.type === 'model_detected'
|
|
5217
|
+
|| candidate.type === 'agent_detected'
|
|
5218
|
+
|| candidate.type === 'passport_match_suggested'
|
|
5219
|
+
|| candidate.type === 'version_candidate_ready'
|
|
5220
|
+
|| candidate.type === 'shadow_candidate'));
|
|
5221
|
+
if (reviewCandidates.length === 0)
|
|
5222
|
+
return null;
|
|
5223
|
+
const modelCount = reviewCandidates.filter((candidate) => candidate.type === 'model_detected').length;
|
|
5224
|
+
const agentCount = reviewCandidates.filter((candidate) => candidate.type === 'agent_detected').length;
|
|
5225
|
+
const runtimeCount = reviewCandidates.filter((candidate) => candidate.type === 'shadow_candidate').length;
|
|
5226
|
+
const confirmationCount = reviewCandidates.filter((candidate) => (candidate.type === 'passport_match_suggested'
|
|
5227
|
+
|| candidate.type === 'version_candidate_ready')).length;
|
|
5228
|
+
const totalReadyCount = this.getMaxNotificationMetadataNumber(reviewCandidates, 'inbox_ready_to_connect_count');
|
|
5229
|
+
const totalConfirmationCount = this.getMaxNotificationMetadataNumber(reviewCandidates, 'inbox_needs_confirmation_count');
|
|
5230
|
+
const totalActionableCount = this.getMaxNotificationMetadataNumber(reviewCandidates, 'inbox_actionable_count');
|
|
5231
|
+
const hasHiddenInboxItems = totalActionableCount > reviewCandidates.length;
|
|
5232
|
+
const redCount = reviewCandidates.filter((candidate) => candidate.severity === 'red').length;
|
|
5233
|
+
const parts = [
|
|
5234
|
+
modelCount > 0 ? this.pluralizeNotificationNoun(modelCount, 'model') : null,
|
|
5235
|
+
agentCount > 0 ? this.pluralizeNotificationNoun(agentCount, 'agent') : null,
|
|
5236
|
+
runtimeCount > 0 ? this.pluralizeNotificationNoun(runtimeCount, 'runtime') : null,
|
|
5237
|
+
confirmationCount > 0 ? this.pluralizeNotificationNoun(confirmationCount, 'confirmation') : null,
|
|
5238
|
+
].filter((item) => Boolean(item));
|
|
5239
|
+
const previewSummary = parts.length > 0 ? parts.join(', ') : this.pluralizeNotificationNoun(reviewCandidates.length, 'item');
|
|
5240
|
+
const inboxSummary = [
|
|
5241
|
+
totalReadyCount > 0 ? `${this.pluralizeNotificationNoun(totalReadyCount, 'item')} ready to connect` : null,
|
|
5242
|
+
totalConfirmationCount > 0 ? `${this.pluralizeNotificationNoun(totalConfirmationCount, 'item')} needing confirmation` : null,
|
|
5243
|
+
].filter((item) => Boolean(item)).join(', ');
|
|
5244
|
+
const itemSummary = hasHiddenInboxItems && inboxSummary
|
|
5245
|
+
? inboxSummary
|
|
5246
|
+
: previewSummary;
|
|
5247
|
+
const reviewSentence = hasHiddenInboxItems && inboxSummary
|
|
5248
|
+
? `${this.pluralizeNotificationNoun(totalActionableCount, 'inbox item')} ${totalActionableCount === 1 ? 'needs' : 'need'} approval or confirmation before registration: ${itemSummary}`
|
|
5249
|
+
: `${itemSummary} ${reviewCandidates.length === 1 ? 'needs' : 'need'} approval or confirmation before registration`;
|
|
5250
|
+
return {
|
|
5251
|
+
id: 'connect-inbox-review',
|
|
5252
|
+
type: modelCount > 0 ? 'model_detected' : agentCount > 0 ? 'agent_detected' : 'shadow_candidate',
|
|
5253
|
+
title: 'Forkit Connect needs your review',
|
|
5254
|
+
message: joinNotificationSentences([
|
|
5255
|
+
reviewSentence,
|
|
5256
|
+
'Nothing has been registered automatically',
|
|
5257
|
+
]),
|
|
5258
|
+
suggested_action: 'open_inbox',
|
|
5259
|
+
severity: redCount > 0 ? 'red' : 'amber',
|
|
5260
|
+
metadata: {
|
|
5261
|
+
notification_group: 'connect_inbox_review',
|
|
5262
|
+
grouped_count: reviewCandidates.length,
|
|
5263
|
+
model_count: modelCount,
|
|
5264
|
+
agent_count: agentCount,
|
|
5265
|
+
runtime_count: runtimeCount,
|
|
5266
|
+
confirmation_count: confirmationCount,
|
|
5267
|
+
inbox_ready_to_connect_count: totalReadyCount,
|
|
5268
|
+
inbox_needs_confirmation_count: totalConfirmationCount,
|
|
5269
|
+
inbox_actionable_count: totalActionableCount,
|
|
5270
|
+
red_count: redCount,
|
|
5271
|
+
relationship_summary: 'Connect inbox items need operator review',
|
|
5272
|
+
source_summary: 'Source: Forkit Connect local discovery',
|
|
5273
|
+
scope_summary: null,
|
|
5274
|
+
},
|
|
5275
|
+
};
|
|
5276
|
+
}
|
|
5277
|
+
buildRuntimeAttentionNotification(candidates) {
|
|
5278
|
+
const runtimeCandidates = candidates.filter((candidate) => (candidate.type === 'runtime_unavailable'
|
|
5279
|
+
&& candidate.severity === 'red'
|
|
5280
|
+
&& (normalizeDisplayText(candidate.metadata.bound_passport_gaid) !== null
|
|
5281
|
+
|| candidate.metadata.binding_status === 'bound'
|
|
5282
|
+
|| candidate.metadata.registration_state === 'registered'
|
|
5283
|
+
|| candidate.metadata.connection_classification === 'registered_runtime')));
|
|
5284
|
+
if (runtimeCandidates.length === 0)
|
|
5285
|
+
return null;
|
|
5286
|
+
const modelCount = runtimeCandidates
|
|
5287
|
+
.filter((candidate) => normalizeDisplayText(candidate.metadata.model_name))
|
|
5288
|
+
.length;
|
|
5289
|
+
return {
|
|
5290
|
+
id: 'runtime-attention',
|
|
5291
|
+
type: 'runtime_unavailable',
|
|
5292
|
+
title: 'Forkit Connect needs runtime attention',
|
|
5293
|
+
message: joinNotificationSentences([
|
|
5294
|
+
`${this.pluralizeNotificationNoun(runtimeCandidates.length, 'runtime')} reported an unavailable state`,
|
|
5295
|
+
modelCount > 0 ? `${this.pluralizeNotificationNoun(modelCount, 'model')} may be affected` : null,
|
|
5296
|
+
]),
|
|
5297
|
+
suggested_action: 'view_pulse_history',
|
|
5298
|
+
severity: 'red',
|
|
5299
|
+
metadata: {
|
|
5300
|
+
notification_group: 'runtime_attention',
|
|
5301
|
+
grouped_count: runtimeCandidates.length,
|
|
5302
|
+
affected_model_count: modelCount,
|
|
5303
|
+
relationship_summary: 'Existing runtime availability changed',
|
|
5304
|
+
source_summary: 'Source: Connect runtime pulse',
|
|
5305
|
+
scope_summary: null,
|
|
5306
|
+
},
|
|
5307
|
+
};
|
|
5308
|
+
}
|
|
5309
|
+
buildGovernanceControlNotification(candidates) {
|
|
5310
|
+
const controlCandidates = candidates.filter((candidate) => (candidate.type === 'session_revoked'
|
|
5311
|
+
|| candidate.type === 'session_paused'));
|
|
5312
|
+
if (controlCandidates.length === 0)
|
|
5313
|
+
return null;
|
|
5314
|
+
const revokedCount = controlCandidates.filter((candidate) => candidate.type === 'session_revoked').length;
|
|
5315
|
+
const pausedCount = controlCandidates.filter((candidate) => candidate.type === 'session_paused').length;
|
|
5316
|
+
const severity = revokedCount > 0 ? 'red' : 'amber';
|
|
5317
|
+
return {
|
|
5318
|
+
id: 'governance-control',
|
|
5319
|
+
type: revokedCount > 0 ? 'session_revoked' : 'session_paused',
|
|
5320
|
+
title: revokedCount > 0 ? 'Forkit governance revoked access' : 'Forkit governance paused communication',
|
|
5321
|
+
message: joinNotificationSentences([
|
|
5322
|
+
revokedCount > 0 ? `${this.pluralizeNotificationNoun(revokedCount, 'session')} revoked` : null,
|
|
5323
|
+
pausedCount > 0 ? `${this.pluralizeNotificationNoun(pausedCount, 'session')} paused` : null,
|
|
5324
|
+
'Review the Connect inbox before continuing governed communication',
|
|
5325
|
+
]),
|
|
5326
|
+
suggested_action: 'open_inbox',
|
|
5327
|
+
severity,
|
|
5328
|
+
metadata: {
|
|
5329
|
+
notification_group: 'governance_control',
|
|
5330
|
+
grouped_count: controlCandidates.length,
|
|
5331
|
+
revoked_count: revokedCount,
|
|
5332
|
+
paused_count: pausedCount,
|
|
5333
|
+
relationship_summary: 'Existing governed sessions need operator attention',
|
|
5334
|
+
source_summary: 'Source: Forkit governance control',
|
|
5335
|
+
scope_summary: null,
|
|
5336
|
+
},
|
|
5337
|
+
};
|
|
5338
|
+
}
|
|
5339
|
+
buildSyncAttentionNotification(candidates) {
|
|
5340
|
+
const syncCandidate = candidates.find((candidate) => (candidate.type === 'c2_sync_pending'
|
|
5341
|
+
&& candidate.severity !== 'grey'));
|
|
5342
|
+
if (!syncCandidate)
|
|
5343
|
+
return null;
|
|
5344
|
+
const pendingEvents = this.getNotificationMetadataNumber(syncCandidate, 'pending_events');
|
|
5345
|
+
return {
|
|
5346
|
+
...syncCandidate,
|
|
5347
|
+
id: 'c2-sync-attention',
|
|
5348
|
+
title: 'Forkit Connect sync needs attention',
|
|
5349
|
+
message: joinNotificationSentences([
|
|
5350
|
+
pendingEvents > 0
|
|
5351
|
+
? `${this.pluralizeNotificationNoun(pendingEvents, 'metadata update')} could not sync cleanly`
|
|
5352
|
+
: 'A governed metadata sync needs attention',
|
|
5353
|
+
'Open Connect to review the local queue',
|
|
5354
|
+
]),
|
|
5355
|
+
metadata: {
|
|
5356
|
+
...syncCandidate.metadata,
|
|
5357
|
+
notification_group: 'c2_sync_attention',
|
|
5358
|
+
relationship_summary: 'Existing local metadata queue needs sync attention',
|
|
5359
|
+
},
|
|
5360
|
+
};
|
|
5361
|
+
}
|
|
5362
|
+
getNotificationDeliveryCandidates(candidates) {
|
|
5363
|
+
const reconnectCandidate = candidates.find((candidate) => candidate.type === 'credential_reconnect_needed');
|
|
5364
|
+
if (reconnectCandidate) {
|
|
5365
|
+
return [{
|
|
5366
|
+
...reconnectCandidate,
|
|
5367
|
+
id: 'account-reconnect',
|
|
5368
|
+
title: 'Forkit Connect needs account approval',
|
|
5369
|
+
message: joinNotificationSentences([
|
|
5370
|
+
'Reconnect this device before governed metadata sync can continue',
|
|
5371
|
+
String(reconnectCandidate.metadata.source_summary || ''),
|
|
5372
|
+
]),
|
|
5373
|
+
metadata: {
|
|
5374
|
+
...reconnectCandidate.metadata,
|
|
5375
|
+
notification_group: 'account_reconnect',
|
|
5376
|
+
},
|
|
5377
|
+
}];
|
|
5378
|
+
}
|
|
5379
|
+
const grouped = [];
|
|
5380
|
+
const governanceNotification = this.buildGovernanceControlNotification(candidates);
|
|
5381
|
+
if (governanceNotification)
|
|
5382
|
+
grouped.push(governanceNotification);
|
|
5383
|
+
const runtimeNotification = this.buildRuntimeAttentionNotification(candidates);
|
|
5384
|
+
if (runtimeNotification)
|
|
5385
|
+
grouped.push(runtimeNotification);
|
|
5386
|
+
const inboxReviewNotification = this.buildConnectInboxReviewNotification(candidates);
|
|
5387
|
+
if (inboxReviewNotification)
|
|
5388
|
+
grouped.push(inboxReviewNotification);
|
|
5389
|
+
const syncNotification = this.buildSyncAttentionNotification(candidates);
|
|
5390
|
+
if (syncNotification)
|
|
5391
|
+
grouped.push(syncNotification);
|
|
5392
|
+
return grouped.slice(0, 3);
|
|
5393
|
+
}
|
|
5394
|
+
getNotificationDeliveryPreview(candidates = this.getNotificationCandidates()) {
|
|
5395
|
+
return this.getNotificationDeliveryCandidates(candidates);
|
|
5396
|
+
}
|
|
4903
5397
|
getNotificationTargetId(candidate) {
|
|
4904
5398
|
const metadata = candidate.metadata;
|
|
5399
|
+
if (metadata.notification_group) {
|
|
5400
|
+
return String(metadata.notification_group);
|
|
5401
|
+
}
|
|
4905
5402
|
switch (candidate.type) {
|
|
4906
5403
|
case 'model_detected':
|
|
4907
5404
|
return String(metadata.discovery_hash || candidate.id);
|
|
@@ -4955,10 +5452,13 @@ class ConnectV1Service {
|
|
|
4955
5452
|
const deliveredAt = options?.deliveredAt ?? nowIso();
|
|
4956
5453
|
let state = this.stateStore.readState();
|
|
4957
5454
|
const candidates = options?.candidates ?? this.getNotificationCandidates();
|
|
5455
|
+
const deliveryCandidates = options?.force && options?.candidates
|
|
5456
|
+
? candidates
|
|
5457
|
+
: this.getNotificationDeliveryCandidates(candidates);
|
|
4958
5458
|
if (!state.connect_config.notifications_enabled && !options?.force) {
|
|
4959
5459
|
return {
|
|
4960
5460
|
delivered: 0,
|
|
4961
|
-
suppressed:
|
|
5461
|
+
suppressed: deliveryCandidates.length,
|
|
4962
5462
|
fallback: false,
|
|
4963
5463
|
disabled: true,
|
|
4964
5464
|
notifications: [],
|
|
@@ -4969,7 +5469,7 @@ class ConnectV1Service {
|
|
|
4969
5469
|
let delivered = 0;
|
|
4970
5470
|
let suppressed = 0;
|
|
4971
5471
|
let fallback = false;
|
|
4972
|
-
for (const candidate of
|
|
5472
|
+
for (const candidate of deliveryCandidates) {
|
|
4973
5473
|
if (!options?.force && this.wasNotificationRecentlyDelivered(state, candidate, deliveredAt)) {
|
|
4974
5474
|
suppressed += 1;
|
|
4975
5475
|
continue;
|
|
@@ -5784,29 +6284,30 @@ class ConnectV1Service {
|
|
|
5784
6284
|
};
|
|
5785
6285
|
}
|
|
5786
6286
|
buildBuildSessionSummary(state, session) {
|
|
6287
|
+
const anchoredSession = this.anchorBuildSessionToBinding(state, session);
|
|
5787
6288
|
const queuedDraft = state.sync_queue.some((item) => item.type === 'passport-draft'
|
|
5788
|
-
&& String(item.payload?.metadata?.build_session_id || '') ===
|
|
5789
|
-
const draftStatus =
|
|
6289
|
+
&& String(item.payload?.metadata?.build_session_id || '') === anchoredSession.build_session_id);
|
|
6290
|
+
const draftStatus = anchoredSession.passport_gaid
|
|
5790
6291
|
? 'bound'
|
|
5791
|
-
:
|
|
6292
|
+
: anchoredSession.draft_id
|
|
5792
6293
|
? 'draft_created'
|
|
5793
6294
|
: queuedDraft
|
|
5794
6295
|
? 'draft_queued'
|
|
5795
6296
|
: 'local_only';
|
|
5796
|
-
const latestLifecycleEvent =
|
|
5797
|
-
const c2Status = this.countC2ForBuildSession(state,
|
|
6297
|
+
const latestLifecycleEvent = anchoredSession.lifecycle_events.at(-1)?.event_type ?? null;
|
|
6298
|
+
const c2Status = this.countC2ForBuildSession(state, anchoredSession.build_session_id);
|
|
5798
6299
|
return {
|
|
5799
|
-
build_session_id:
|
|
5800
|
-
model_name:
|
|
5801
|
-
framework:
|
|
5802
|
-
task:
|
|
5803
|
-
build_status:
|
|
6300
|
+
build_session_id: anchoredSession.build_session_id,
|
|
6301
|
+
model_name: anchoredSession.model_name,
|
|
6302
|
+
framework: anchoredSession.framework,
|
|
6303
|
+
task: anchoredSession.task,
|
|
6304
|
+
build_status: anchoredSession.status,
|
|
5804
6305
|
draft_status: draftStatus,
|
|
5805
|
-
draft_id:
|
|
5806
|
-
passport_gaid:
|
|
5807
|
-
dataset_refs_count:
|
|
5808
|
-
versions_count:
|
|
5809
|
-
metrics_count:
|
|
6306
|
+
draft_id: anchoredSession.draft_id,
|
|
6307
|
+
passport_gaid: anchoredSession.passport_gaid,
|
|
6308
|
+
dataset_refs_count: anchoredSession.dataset_refs.length,
|
|
6309
|
+
versions_count: anchoredSession.versions.length,
|
|
6310
|
+
metrics_count: anchoredSession.metrics.length,
|
|
5810
6311
|
latest_lifecycle_event: latestLifecycleEvent,
|
|
5811
6312
|
c2_sync_status: c2Status.pending > 0 || c2Status.synced > 0 || c2Status.lastError
|
|
5812
6313
|
? {
|
|
@@ -5815,14 +6316,16 @@ class ConnectV1Service {
|
|
|
5815
6316
|
last_error: c2Status.lastError,
|
|
5816
6317
|
}
|
|
5817
6318
|
: null,
|
|
5818
|
-
updated_at:
|
|
6319
|
+
updated_at: anchoredSession.updated_at,
|
|
5819
6320
|
};
|
|
5820
6321
|
}
|
|
5821
6322
|
saveBuildSession(session) {
|
|
5822
|
-
this.stateStore.
|
|
6323
|
+
const state = this.stateStore.readState();
|
|
6324
|
+
const anchored = this.anchorBuildSessionToBinding(state, {
|
|
5823
6325
|
...session,
|
|
5824
6326
|
updated_at: session.updated_at || nowIso(),
|
|
5825
6327
|
});
|
|
6328
|
+
this.stateStore.upsertBuildSession(anchored);
|
|
5826
6329
|
return this.requireBuildSession(this.stateStore.readState(), session.build_session_id);
|
|
5827
6330
|
}
|
|
5828
6331
|
shouldFallbackToQueuedTrainingDraft(status) {
|
|
@@ -5862,7 +6365,7 @@ class ConnectV1Service {
|
|
|
5862
6365
|
],
|
|
5863
6366
|
artifact_refs: [],
|
|
5864
6367
|
};
|
|
5865
|
-
this.saveBuildSession(session);
|
|
6368
|
+
session = this.saveBuildSession(session);
|
|
5866
6369
|
this.stateStore.addEvidenceEvent({
|
|
5867
6370
|
type: 'training_session_initialized',
|
|
5868
6371
|
details: {
|
|
@@ -5875,19 +6378,30 @@ class ConnectV1Service {
|
|
|
5875
6378
|
this.queueTrainingC2Event(session, 'connect_training_initialized', {
|
|
5876
6379
|
dataset_refs_count: datasetRefs.length,
|
|
5877
6380
|
}, now);
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
6381
|
+
if (!session.passport_gaid) {
|
|
6382
|
+
this.recordC2LifecycleEvent({
|
|
6383
|
+
eventType: 'connect_passport_draft_requested',
|
|
6384
|
+
modelName: session.model_name,
|
|
6385
|
+
passportGaid: null,
|
|
6386
|
+
workspaceId: session.workspaceId,
|
|
6387
|
+
projectId: session.projectId,
|
|
6388
|
+
metadata: {
|
|
6389
|
+
build_session_id: session.build_session_id,
|
|
6390
|
+
source_mode: 'connect_training',
|
|
6391
|
+
connection_status: 'building',
|
|
6392
|
+
},
|
|
6393
|
+
});
|
|
6394
|
+
}
|
|
5890
6395
|
const payload = this.buildTrainingDraftPayload(session);
|
|
6396
|
+
if (session.passport_gaid) {
|
|
6397
|
+
return {
|
|
6398
|
+
buildSession: session,
|
|
6399
|
+
draftAction: 'bound',
|
|
6400
|
+
draftId: null,
|
|
6401
|
+
gaid: session.passport_gaid,
|
|
6402
|
+
payload,
|
|
6403
|
+
};
|
|
6404
|
+
}
|
|
5891
6405
|
if (this.hasSessionRef()) {
|
|
5892
6406
|
const result = await this.getApiClient(state).createPassportDraft(payload);
|
|
5893
6407
|
if (!result.ok) {
|
|
@@ -6123,13 +6637,21 @@ class ConnectV1Service {
|
|
|
6123
6637
|
}
|
|
6124
6638
|
getTrainingStatus(buildSessionId) {
|
|
6125
6639
|
const state = this.stateStore.readState();
|
|
6640
|
+
const maybePersistAnchoredSession = (session) => {
|
|
6641
|
+
const anchored = this.anchorBuildSessionToBinding(state, session);
|
|
6642
|
+
if (anchored !== session) {
|
|
6643
|
+
this.stateStore.upsertBuildSession(anchored);
|
|
6644
|
+
return anchored;
|
|
6645
|
+
}
|
|
6646
|
+
return session;
|
|
6647
|
+
};
|
|
6126
6648
|
if (buildSessionId) {
|
|
6127
|
-
const session = this.requireBuildSession(state, buildSessionId);
|
|
6649
|
+
const session = maybePersistAnchoredSession(this.requireBuildSession(state, buildSessionId));
|
|
6128
6650
|
return this.buildBuildSessionSummary(state, session);
|
|
6129
6651
|
}
|
|
6130
6652
|
return [...state.build_sessions]
|
|
6131
6653
|
.sort((left, right) => right.updated_at.localeCompare(left.updated_at))
|
|
6132
|
-
.map((session) => this.buildBuildSessionSummary(state, session));
|
|
6654
|
+
.map((session) => this.buildBuildSessionSummary(state, maybePersistAnchoredSession(session)));
|
|
6133
6655
|
}
|
|
6134
6656
|
markBuildSessionDraftPublished(draftId, passportGaid, workspaceId, projectId) {
|
|
6135
6657
|
const state = this.stateStore.readState();
|
|
@@ -7782,6 +8304,7 @@ class ConnectV1Service {
|
|
|
7782
8304
|
const result = options?.processScoutResults
|
|
7783
8305
|
? await this.scanRuntime({ processScoutResults: options.processScoutResults })
|
|
7784
8306
|
: await this.scanRuntime();
|
|
8307
|
+
this.syncEvolutionCandidates();
|
|
7785
8308
|
const queue = result.discoveryMode === 'auto_draft'
|
|
7786
8309
|
? this.queueModelDraftsFromDetected()
|
|
7787
8310
|
: emptyQueueSummary();
|
|
@@ -8129,7 +8652,7 @@ class ConnectV1Service {
|
|
|
8129
8652
|
const secureStorage = this.credentialStore.getStatus();
|
|
8130
8653
|
checks.push({
|
|
8131
8654
|
name: 'secure_storage',
|
|
8132
|
-
status: secureStorage.available ? '
|
|
8655
|
+
status: !secureStorage.available ? 'FAIL' : secureStorage.plaintextFallbackActive ? 'WARN' : 'PASS',
|
|
8133
8656
|
details: secureStorage.legacyPlaintextFilePresent
|
|
8134
8657
|
? `${secureStorage.detail} Legacy plaintext credential file still detected at ${this.credentialStore.getCredentialFilePath()}.`
|
|
8135
8658
|
: secureStorage.detail,
|
|
@@ -8165,6 +8688,11 @@ class ConnectV1Service {
|
|
|
8165
8688
|
status: 'WARN',
|
|
8166
8689
|
details: 'No session reference stored. Set FORKIT_CONNECT_SESSION_REF or run connect with session binding.',
|
|
8167
8690
|
});
|
|
8691
|
+
checks.push({
|
|
8692
|
+
name: 'backend_session_truth',
|
|
8693
|
+
status: 'WARN',
|
|
8694
|
+
details: 'session_state=missing. Local scope (if present) is cached-only until login verifies account truth.',
|
|
8695
|
+
});
|
|
8168
8696
|
return checks;
|
|
8169
8697
|
}
|
|
8170
8698
|
checks.push({
|
|
@@ -8174,11 +8702,39 @@ class ConnectV1Service {
|
|
|
8174
8702
|
? 'Using session reference from FORKIT_CONNECT_SESSION_REF. Headless mode can run without local credential persistence.'
|
|
8175
8703
|
: 'Secure session reference is present in local credential storage.',
|
|
8176
8704
|
});
|
|
8705
|
+
const api = new api_1.ConnectApiClient({
|
|
8706
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
8707
|
+
sessionRef: this.readSessionRef(),
|
|
8708
|
+
});
|
|
8709
|
+
let sessionTruth = 'unavailable';
|
|
8710
|
+
let profileStatus = null;
|
|
8711
|
+
try {
|
|
8712
|
+
const profileAccess = await api.getProfileAccess();
|
|
8713
|
+
profileStatus = profileAccess.status;
|
|
8714
|
+
if (profileAccess.ok) {
|
|
8715
|
+
sessionTruth = 'authorized';
|
|
8716
|
+
}
|
|
8717
|
+
else if (profileAccess.status === 401 || profileAccess.status === 403) {
|
|
8718
|
+
sessionTruth = 'expired';
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
catch {
|
|
8722
|
+
sessionTruth = 'unavailable';
|
|
8723
|
+
profileStatus = null;
|
|
8724
|
+
}
|
|
8725
|
+
checks.push({
|
|
8726
|
+
name: 'backend_session_truth',
|
|
8727
|
+
status: sessionTruth === 'authorized' ? 'PASS' : 'WARN',
|
|
8728
|
+
details: sessionTruth === 'authorized'
|
|
8729
|
+
? `session_state=authorized via GET /api/profiles/access (${profileStatus ?? 'ok'})`
|
|
8730
|
+
: sessionTruth === 'expired'
|
|
8731
|
+
? `session_state=expired via GET /api/profiles/access (${profileStatus ?? 'unknown'}). Renew login before governed actions.`
|
|
8732
|
+
: `session_state=unavailable via GET /api/profiles/access (${profileStatus ?? 'timeout_or_network_error'}). Account truth is temporarily offline.`,
|
|
8733
|
+
});
|
|
8734
|
+
if (sessionTruth !== 'authorized') {
|
|
8735
|
+
return checks;
|
|
8736
|
+
}
|
|
8177
8737
|
try {
|
|
8178
|
-
const api = new api_1.ConnectApiClient({
|
|
8179
|
-
baseUrl: DEFAULT_BASE_URL,
|
|
8180
|
-
sessionRef: this.readSessionRef(),
|
|
8181
|
-
});
|
|
8182
8738
|
const mine = await api.getPassportsMine();
|
|
8183
8739
|
checks.push({
|
|
8184
8740
|
name: 'backend_passports_mine',
|