rhombus-node-mcp 0.1.50 → 0.1.51
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/dist/api/elements-access-anomaly-tool-api.js +105 -0
- package/dist/api/elements-badge-timeline-tool-api.js +56 -0
- package/dist/api/elements-lost-badge-tool-api.js +86 -0
- package/dist/api/elements-tool-api.js +64 -0
- package/dist/api/onguard-tool-api.js +16 -2
- package/dist/tools-console/elements-access-anomaly-tool.js +53 -0
- package/dist/tools-console/elements-badge-timeline-tool.js +50 -0
- package/dist/tools-console/elements-lost-badge-tool.js +47 -0
- package/dist/tools-console/elements-tool.js +55 -0
- package/dist/types/elements-access-anomaly-tool-types.js +3 -0
- package/dist/types/elements-badge-timeline-tool-types.js +3 -0
- package/dist/types/elements-lost-badge-tool-types.js +3 -0
- package/dist/types/elements-tool-types.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { buildMediaHints } from "./badge-correlation.js";
|
|
2
|
+
import { searchElementsEvents } from "./elements-tool-api.js";
|
|
3
|
+
const ALL_RULES = [
|
|
4
|
+
"lost_or_inactive_badge",
|
|
5
|
+
"entry_not_made",
|
|
6
|
+
"off_hours",
|
|
7
|
+
"impossible_travel",
|
|
8
|
+
"area_novelty",
|
|
9
|
+
];
|
|
10
|
+
const DAY_MS = 86_400_000;
|
|
11
|
+
/**
|
|
12
|
+
* Flags anomalous Honeywell Elements access events over a window: deterministic rules (lost/inactive badge,
|
|
13
|
+
* no-entry, off-hours, impossible travel, first-time-in-area vs a baseline) computed over searchElementsEvents,
|
|
14
|
+
* each finding carrying the still/clip hints to confirm it. The agent narrates/triages from here.
|
|
15
|
+
*/
|
|
16
|
+
export async function getElementsAccessAnomalies(args, timeZone, requestModifiers, sessionId) {
|
|
17
|
+
const rules = new Set(args.rules && args.rules.length ? args.rules : ALL_RULES);
|
|
18
|
+
const offStart = args.offHoursStartHour ?? 7;
|
|
19
|
+
const offEnd = args.offHoursEndHour ?? 19;
|
|
20
|
+
const maxTravelSec = args.impossibleTravelMaxSeconds ?? 30;
|
|
21
|
+
const baselineDays = args.baselineDays ?? 30;
|
|
22
|
+
const scope = { area: args.area, locationUuids: args.locationUuids, deviceUuids: args.deviceUuids };
|
|
23
|
+
const { events } = await searchElementsEvents({ ...scope, afterMs: args.afterMs, beforeMs: args.beforeMs, limit: args.limit ?? 500 }, timeZone, requestModifiers, sessionId);
|
|
24
|
+
// Baseline area-set per cardholder (for area_novelty).
|
|
25
|
+
const baselineAreas = new Map();
|
|
26
|
+
if (rules.has("area_novelty") && baselineDays > 0 && args.afterMs != null) {
|
|
27
|
+
const { events: baseEvents } = await searchElementsEvents({ ...scope, afterMs: args.afterMs - baselineDays * DAY_MS, beforeMs: args.afterMs, limit: 1000 }, timeZone, requestModifiers, sessionId);
|
|
28
|
+
for (const e of baseEvents) {
|
|
29
|
+
if (!e.cardholderName || !e.areaEntering)
|
|
30
|
+
continue;
|
|
31
|
+
const set = baselineAreas.get(e.cardholderName) ?? new Set();
|
|
32
|
+
set.add(e.areaEntering);
|
|
33
|
+
baselineAreas.set(e.cardholderName, set);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const hourFmt = new Intl.DateTimeFormat("en-US", { timeZone, hour: "2-digit", hourCycle: "h23" });
|
|
37
|
+
const localHour = (ms) => parseInt(hourFmt.format(new Date(ms)), 10);
|
|
38
|
+
const findings = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const add = (e, rule, severity, rationale) => {
|
|
41
|
+
const key = `${e.cardholderName ?? ""}|${rule}|${e.timestampMs ?? ""}|${e.areaEntering ?? ""}`;
|
|
42
|
+
if (seen.has(key))
|
|
43
|
+
return;
|
|
44
|
+
seen.add(key);
|
|
45
|
+
const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs });
|
|
46
|
+
findings.push({
|
|
47
|
+
cardholderName: e.cardholderName,
|
|
48
|
+
rule,
|
|
49
|
+
severity,
|
|
50
|
+
datetime: e.datetime,
|
|
51
|
+
timestampMs: e.timestampMs,
|
|
52
|
+
deviceUuid: e.deviceUuid,
|
|
53
|
+
area: e.areaEntering ?? e.areaExiting,
|
|
54
|
+
rationale,
|
|
55
|
+
clipHint,
|
|
56
|
+
stillHint,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
for (const e of events) {
|
|
60
|
+
const at = e.areaEntering ? ` at "${e.areaEntering}"` : "";
|
|
61
|
+
if (rules.has("lost_or_inactive_badge") && e.badgeStatus && e.badgeStatus.toLowerCase() !== "active") {
|
|
62
|
+
add(e, "lost_or_inactive_badge", "high", `Badge status "${e.badgeStatus}" used${at}.`);
|
|
63
|
+
}
|
|
64
|
+
if (rules.has("entry_not_made") && e.entryMade === false) {
|
|
65
|
+
add(e, "entry_not_made", "medium", `Access granted but no entry made${at} — possible tailgating or aborted entry.`);
|
|
66
|
+
}
|
|
67
|
+
if (rules.has("off_hours") && e.timestampMs != null) {
|
|
68
|
+
const h = localHour(e.timestampMs);
|
|
69
|
+
if (h < offStart || h >= offEnd) {
|
|
70
|
+
add(e, "off_hours", "medium", `Entry ${e.datetime ?? `${h}:00`} is outside business hours (${offStart}:00–${offEnd}:00)${at}.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (rules.has("area_novelty") && baselineAreas.size > 0 && e.cardholderName && e.areaEntering) {
|
|
74
|
+
const base = baselineAreas.get(e.cardholderName);
|
|
75
|
+
if (base && !base.has(e.areaEntering)) {
|
|
76
|
+
add(e, "area_novelty", "high", `First entry to "${e.areaEntering}" — not in ${e.cardholderName}'s prior ${baselineDays}-day pattern.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Impossible travel: per cardholder, consecutive taps in different areas within the window.
|
|
81
|
+
if (rules.has("impossible_travel")) {
|
|
82
|
+
const byPerson = new Map();
|
|
83
|
+
for (const e of events) {
|
|
84
|
+
if (!e.cardholderName || e.timestampMs == null)
|
|
85
|
+
continue;
|
|
86
|
+
const arr = byPerson.get(e.cardholderName) ?? [];
|
|
87
|
+
arr.push(e);
|
|
88
|
+
byPerson.set(e.cardholderName, arr);
|
|
89
|
+
}
|
|
90
|
+
for (const [name, evs] of byPerson) {
|
|
91
|
+
const sorted = evs.slice().sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
|
|
92
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
93
|
+
const a = sorted[i - 1];
|
|
94
|
+
const b = sorted[i];
|
|
95
|
+
const gapSec = ((b.timestampMs ?? 0) - (a.timestampMs ?? 0)) / 1000;
|
|
96
|
+
if (a.areaEntering && b.areaEntering && a.areaEntering !== b.areaEntering && gapSec <= maxTravelSec) {
|
|
97
|
+
add(b, "impossible_travel", "high", `${name} at "${a.areaEntering}" then "${b.areaEntering}" ${Math.round(gapSec)}s apart — physically implausible for one person.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const sev = (s) => (s === "high" ? 0 : 1);
|
|
103
|
+
findings.sort((a, b) => sev(a.severity) - sev(b.severity) || (b.timestampMs ?? 0) - (a.timestampMs ?? 0));
|
|
104
|
+
return { findings, eventsAnalyzed: events.length };
|
|
105
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { buildMediaHints } from "./badge-correlation.js";
|
|
2
|
+
import { searchElementsEvents } from "./elements-tool-api.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reconstructs a single cardholder's movements as a chronological timeline of Honeywell Elements badge taps,
|
|
5
|
+
* each with the still/clip hints needed to show what happened. Pure orchestration over
|
|
6
|
+
* searchElementsEvents + the shared correlation helper.
|
|
7
|
+
*/
|
|
8
|
+
export async function getElementsBadgeTimeline(args, timeZone, requestModifiers, sessionId) {
|
|
9
|
+
const { events } = await searchElementsEvents({
|
|
10
|
+
cardholderQuery: args.cardholderQuery,
|
|
11
|
+
locationUuids: args.locationUuids,
|
|
12
|
+
afterMs: args.afterMs,
|
|
13
|
+
beforeMs: args.beforeMs,
|
|
14
|
+
limit: args.limit ?? 200,
|
|
15
|
+
}, timeZone, requestModifiers, sessionId);
|
|
16
|
+
// searchElementsEvents returns newest-first; a timeline reads chronologically.
|
|
17
|
+
const ordered = events
|
|
18
|
+
.filter((e) => e.timestampMs != null)
|
|
19
|
+
.sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
|
|
20
|
+
const stops = ordered.map((e, i) => {
|
|
21
|
+
const next = ordered[i + 1];
|
|
22
|
+
const gapToNextSeconds = next?.timestampMs != null && e.timestampMs != null
|
|
23
|
+
? Math.round((next.timestampMs - e.timestampMs) / 1000)
|
|
24
|
+
: undefined;
|
|
25
|
+
const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs }, args.clipPaddingSeconds);
|
|
26
|
+
return {
|
|
27
|
+
timestampMs: e.timestampMs,
|
|
28
|
+
datetime: e.datetime,
|
|
29
|
+
deviceUuid: e.deviceUuid,
|
|
30
|
+
area: e.areaEntering ?? e.areaExiting,
|
|
31
|
+
label: e.label,
|
|
32
|
+
isAnomaly: e.isAnomaly,
|
|
33
|
+
clipHint,
|
|
34
|
+
stillHint,
|
|
35
|
+
gapToNextSeconds,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
// The sequence of areas traversed, collapsing consecutive repeats.
|
|
39
|
+
const path = [];
|
|
40
|
+
for (const stop of stops) {
|
|
41
|
+
if (stop.area && stop.area !== path[path.length - 1]) {
|
|
42
|
+
path.push(stop.area);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// cardholderQuery is full-text, so it can match more than one person — surface that so the agent
|
|
46
|
+
// can disambiguate rather than silently merge two people's movements.
|
|
47
|
+
const distinctCardholders = [
|
|
48
|
+
...new Set(ordered.map((e) => e.cardholderName).filter((n) => !!n)),
|
|
49
|
+
];
|
|
50
|
+
return {
|
|
51
|
+
cardholderName: distinctCardholders[0],
|
|
52
|
+
ambiguousCardholders: distinctCardholders.length > 1 ? distinctCardholders : undefined,
|
|
53
|
+
stops,
|
|
54
|
+
path,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { buildMediaHints } from "./badge-correlation.js";
|
|
2
|
+
import { searchElementsEvents } from "./elements-tool-api.js";
|
|
3
|
+
import { getFaceEvents, searchSimilarFaces } from "./faces-tool-api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Lost / stolen-badge live response for Honeywell Elements. Finds recent lost/inactive-badge use, and for
|
|
6
|
+
* each: the door clip/still, the face captured at the door (identified or UNIDENTIFIED), and a cross-camera
|
|
7
|
+
* track of that same face (via similar-face search) ordered to a last-known location. Detection + door
|
|
8
|
+
* evidence are exact; the track is best-effort (depends on face capture + recognition coverage).
|
|
9
|
+
*/
|
|
10
|
+
export async function getElementsLostBadgeResponse(args, timeZone, requestModifiers, sessionId) {
|
|
11
|
+
const scope = { area: args.area, locationUuids: args.locationUuids, deviceUuids: args.deviceUuids };
|
|
12
|
+
const { events } = await searchElementsEvents({ ...scope, anomalyOnly: true, afterMs: args.afterMs, beforeMs: args.beforeMs, limit: args.limit ?? 50 }, timeZone, requestModifiers, sessionId);
|
|
13
|
+
// anomalyOnly returns lost-badge AND no-entry anomalies; keep the lost/inactive-badge ones. Elements
|
|
14
|
+
// credentialStatus uses values like Lost/Stolen/Terminated — anything that isn't "active".
|
|
15
|
+
const lostEvents = events.filter((e) => e.badgeStatus && e.badgeStatus.toLowerCase() !== "active");
|
|
16
|
+
const winMs = (args.faceWindowSeconds ?? 30) * 1000;
|
|
17
|
+
const incidents = [];
|
|
18
|
+
for (const e of lostEvents) {
|
|
19
|
+
const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs });
|
|
20
|
+
let facesAtDoor = [];
|
|
21
|
+
let sightings = [];
|
|
22
|
+
if (e.deviceUuid && e.timestampMs != null) {
|
|
23
|
+
try {
|
|
24
|
+
// Face(s) at the door: query by time window (the faces tool can't filter by device), then match
|
|
25
|
+
// the door camera client-side.
|
|
26
|
+
const { faceEvents } = await getFaceEvents({
|
|
27
|
+
pageRequest: { lastEvaluatedKey: null, maxPageSize: 75 },
|
|
28
|
+
searchFilter: {
|
|
29
|
+
faceNameContains: null,
|
|
30
|
+
faceNames: [],
|
|
31
|
+
hasEmbedding: null,
|
|
32
|
+
hasName: null,
|
|
33
|
+
labels: [],
|
|
34
|
+
locationUuids: [],
|
|
35
|
+
personUuids: [],
|
|
36
|
+
timestampFilter: {
|
|
37
|
+
rangeStart: new Date(e.timestampMs - winMs).toISOString(),
|
|
38
|
+
rangeEnd: new Date(e.timestampMs + winMs).toISOString(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}, timeZone, requestModifiers, sessionId);
|
|
42
|
+
facesAtDoor = faceEvents
|
|
43
|
+
.filter((f) => f.deviceUuid === e.deviceUuid)
|
|
44
|
+
.map((f) => ({
|
|
45
|
+
faceName: f.faceName ?? undefined,
|
|
46
|
+
personUuid: f.personUuid ?? undefined,
|
|
47
|
+
thumbnailS3Key: f.thumbnailS3Key ?? undefined,
|
|
48
|
+
faceEventUuid: f.uuid ?? undefined,
|
|
49
|
+
eventTimestamp: f.eventTimestamp,
|
|
50
|
+
}));
|
|
51
|
+
// Track that face across cameras, ordered in time.
|
|
52
|
+
const seed = facesAtDoor.find((f) => f.faceEventUuid);
|
|
53
|
+
if (seed?.faceEventUuid) {
|
|
54
|
+
const similar = await searchSimilarFaces(seed.faceEventUuid, timeZone, requestModifiers, sessionId);
|
|
55
|
+
sightings = similar
|
|
56
|
+
.map((s) => ({
|
|
57
|
+
deviceUuid: s.deviceUuid,
|
|
58
|
+
datetime: s.eventTimestamp,
|
|
59
|
+
timestampMs: s.eventTimestampMs,
|
|
60
|
+
similarity: s.similarity,
|
|
61
|
+
personUuid: s.personUuid,
|
|
62
|
+
}))
|
|
63
|
+
.sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Face enrichment is best-effort — never fail the whole response over it.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const last = sightings.length ? sightings[sightings.length - 1] : undefined;
|
|
71
|
+
incidents.push({
|
|
72
|
+
cardholderOfRecord: e.cardholderName,
|
|
73
|
+
badgeStatus: e.badgeStatus,
|
|
74
|
+
datetime: e.datetime,
|
|
75
|
+
timestampMs: e.timestampMs,
|
|
76
|
+
deviceUuid: e.deviceUuid,
|
|
77
|
+
area: e.areaEntering ?? e.areaExiting,
|
|
78
|
+
clipHint,
|
|
79
|
+
stillHint,
|
|
80
|
+
facesAtDoor,
|
|
81
|
+
sightings,
|
|
82
|
+
lastKnownSighting: last ? { deviceUuid: last.deviceUuid, datetime: last.datetime } : undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { incidents, count: incidents.length };
|
|
86
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { postApi } from "../network/network.js";
|
|
2
|
+
import { formatTimestamp } from "../util.js";
|
|
3
|
+
/**
|
|
4
|
+
* Honeywell Elements (LenelS2 Elements) activity enum values. Passing these as `activityTypes` to the
|
|
5
|
+
* generalized access-control event search scopes results to Elements events only — exactly mirroring the
|
|
6
|
+
* OnGuard search, which is implicitly scoped to ONGUARD_* types server-side.
|
|
7
|
+
*/
|
|
8
|
+
export const ELEMENTS_ACTIVITY_TYPES = [
|
|
9
|
+
"ELEMENTS_BADGE_AUTHORIZED",
|
|
10
|
+
"ELEMENTS_BADGE_ANOMALY",
|
|
11
|
+
"ELEMENTS_NO_ENTRY_MADE",
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Calls the generalized webservice access-control event search
|
|
15
|
+
* (POST /eventSearchV2/searchIntegrationAccessEvents) scoped to Honeywell Elements via `activityTypes`, and
|
|
16
|
+
* maps the raw seekpoints to the same agent-friendly shape as searchOnGuardEvents. The request DTO mirrors
|
|
17
|
+
* Eventsearch_SearchOnGuardEventsWSRequest plus an `activityTypes` array; the response shape is identical.
|
|
18
|
+
*
|
|
19
|
+
* The generalized endpoint, its `activityTypes` field, and the ELEMENTS_* enum values predate the generated
|
|
20
|
+
* public OpenAPI schema, so the body is built off the OnGuard request DTO and the extra field is added via a
|
|
21
|
+
* cast until `assets/openapi.json` is regenerated.
|
|
22
|
+
*/
|
|
23
|
+
export async function searchElementsEvents(args, timeZone, requestModifiers, sessionId) {
|
|
24
|
+
const body = {
|
|
25
|
+
deviceUuids: args.deviceUuids,
|
|
26
|
+
locationUuids: args.locationUuids,
|
|
27
|
+
afterMs: args.afterMs,
|
|
28
|
+
beforeMs: args.beforeMs,
|
|
29
|
+
cardholderQuery: args.cardholderQuery,
|
|
30
|
+
badgeStatus: args.badgeStatus,
|
|
31
|
+
badgeType: args.badgeType,
|
|
32
|
+
area: args.area,
|
|
33
|
+
anomalyOnly: args.anomalyOnly,
|
|
34
|
+
entryMade: args.entryMade,
|
|
35
|
+
limit: args.limit ?? 200,
|
|
36
|
+
activityTypes: [...ELEMENTS_ACTIVITY_TYPES],
|
|
37
|
+
};
|
|
38
|
+
const res = await postApi({
|
|
39
|
+
route: "/eventSearchV2/searchIntegrationAccessEvents",
|
|
40
|
+
body,
|
|
41
|
+
modifiers: requestModifiers,
|
|
42
|
+
sessionId,
|
|
43
|
+
});
|
|
44
|
+
if (res.error) {
|
|
45
|
+
throw new Error(res.status ?? res.errorMsg ?? "Elements event search failed");
|
|
46
|
+
}
|
|
47
|
+
const events = (res.events ?? []).map((e) => ({
|
|
48
|
+
timestampMs: e.timestampMs ?? undefined,
|
|
49
|
+
datetime: e.timestampMs != null ? formatTimestamp(e.timestampMs, timeZone) : undefined,
|
|
50
|
+
deviceUuid: e.deviceUuid ?? undefined,
|
|
51
|
+
label: e.customDisplayName ?? e.objectType ?? undefined,
|
|
52
|
+
// Dedicated-event-types put the cardholder in its own `cardholderName` field (customDescription
|
|
53
|
+
// is null on those docs); keep customDescription as a legacy fallback. The generated schema
|
|
54
|
+
// predates the field, so read it through a cast until `assets/openapi.json` is regenerated.
|
|
55
|
+
cardholderName: e.cardholderName ?? e.customDescription ?? undefined,
|
|
56
|
+
badgeStatus: e.badgeStatus ?? undefined,
|
|
57
|
+
badgeType: e.badgeType ?? undefined,
|
|
58
|
+
areaEntering: e.areaEntering ?? undefined,
|
|
59
|
+
areaExiting: e.areaExiting ?? undefined,
|
|
60
|
+
entryMade: e.entryMade ?? undefined,
|
|
61
|
+
isAnomaly: e.alert ?? undefined,
|
|
62
|
+
}));
|
|
63
|
+
return { events };
|
|
64
|
+
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { postApi } from "../network/network.js";
|
|
2
2
|
import { formatTimestamp } from "../util.js";
|
|
3
3
|
/**
|
|
4
|
-
* Calls the
|
|
4
|
+
* Calls the unified integration access-event search (POST /eventSearchV2/searchIntegrationAccessEvents),
|
|
5
|
+
* scoped to OnGuard via `activityTypes`, and maps the
|
|
5
6
|
* raw seekpoints to an agent-friendly shape. Typed against the generated public OpenAPI schema.
|
|
6
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Honeywell OnGuard (Lenel) activity enum values. Passing these as `activityTypes` to the generalized
|
|
10
|
+
* integration access-event search scopes results to OnGuard events only.
|
|
11
|
+
*/
|
|
12
|
+
export const ONGUARD_ACTIVITY_TYPES = [
|
|
13
|
+
"ONGUARD_BADGE_AUTHORIZED",
|
|
14
|
+
"ONGUARD_BADGE_ANOMALY",
|
|
15
|
+
"ONGUARD_NO_ENTRY_MADE",
|
|
16
|
+
];
|
|
7
17
|
export async function searchOnGuardEvents(args, timeZone, requestModifiers, sessionId) {
|
|
18
|
+
// Unified third-party integration access-event search, scoped to OnGuard via `activityTypes`. The
|
|
19
|
+
// generalized endpoint + `activityTypes` predate the generated schema, so the extra field is added via
|
|
20
|
+
// a cast off the OnGuard request DTO until `assets/openapi.json` is regenerated.
|
|
8
21
|
const body = {
|
|
9
22
|
deviceUuids: args.deviceUuids,
|
|
10
23
|
locationUuids: args.locationUuids,
|
|
@@ -17,9 +30,10 @@ export async function searchOnGuardEvents(args, timeZone, requestModifiers, sess
|
|
|
17
30
|
anomalyOnly: args.anomalyOnly,
|
|
18
31
|
entryMade: args.entryMade,
|
|
19
32
|
limit: args.limit ?? 200,
|
|
33
|
+
activityTypes: [...ONGUARD_ACTIVITY_TYPES],
|
|
20
34
|
};
|
|
21
35
|
const res = await postApi({
|
|
22
|
-
route: "/eventSearchV2/
|
|
36
|
+
route: "/eventSearchV2/searchIntegrationAccessEvents",
|
|
23
37
|
body,
|
|
24
38
|
modifiers: requestModifiers,
|
|
25
39
|
sessionId,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getElementsAccessAnomalies } from "../api/elements-access-anomaly-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/elements-access-anomaly-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "elements-access-anomaly-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Scans Honeywell Elements (LenelS2 Elements) access events over a time window and flags anomalous badge activity. Use for
|
|
7
|
+
"find unusual badge activity", "anything suspicious in access this week", or proactive access review.
|
|
8
|
+
|
|
9
|
+
Runs deterministic rules and returns ranked findings (high severity first):
|
|
10
|
+
- lost_or_inactive_badge: a non-active badge (lost/suspended) was used
|
|
11
|
+
- entry_not_made: access granted but no entry made (possible tailgating)
|
|
12
|
+
- off_hours: entry outside business hours (configurable)
|
|
13
|
+
- impossible_travel: one cardholder at two different areas seconds apart
|
|
14
|
+
- area_novelty: a cardholder's first-ever entry to an area vs their prior history (needs a baseline window)
|
|
15
|
+
|
|
16
|
+
Each finding includes cardholderName, the rule, severity, datetime, area, the camera deviceUuid, a plain-language
|
|
17
|
+
rationale, and clip/still hints. Resolve relative times (e.g. "this week") to ISO 8601 first via the timestamp tool.
|
|
18
|
+
|
|
19
|
+
This is a triage aid: present findings grouped by severity, and for the notable ones call the camera-tool
|
|
20
|
+
(requestType "image", cameraUuid = finding.deviceUuid, timestamp = finding.timestampMs) and/or clips-tool
|
|
21
|
+
("createClip" using finding.clipHint) — in PARALLEL — so a human can confirm. Don't assert wrongdoing; surface the
|
|
22
|
+
evidence.
|
|
23
|
+
`;
|
|
24
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
25
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
26
|
+
try {
|
|
27
|
+
const result = await getElementsAccessAnomalies({
|
|
28
|
+
area: args.area ?? undefined,
|
|
29
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
30
|
+
deviceUuids: args.deviceUuids ?? undefined,
|
|
31
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
32
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
33
|
+
rules: args.rules ?? undefined,
|
|
34
|
+
baselineDays: args.baselineDays ?? undefined,
|
|
35
|
+
offHoursStartHour: args.offHoursStartHour ?? undefined,
|
|
36
|
+
offHoursEndHour: args.offHoursEndHour ?? undefined,
|
|
37
|
+
impossibleTravelMaxSeconds: args.impossibleTravelMaxSeconds ?? undefined,
|
|
38
|
+
limit: args.limit ?? undefined,
|
|
39
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
40
|
+
return createToolStructuredContent(result);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
44
|
+
return createToolStructuredContent({ error: message });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export function createTool(server) {
|
|
48
|
+
server.registerTool(TOOL_NAME, {
|
|
49
|
+
description: TOOL_DESCRIPTION,
|
|
50
|
+
inputSchema: TOOL_ARGS,
|
|
51
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
52
|
+
}, TOOL_HANDLER);
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getElementsBadgeTimeline } from "../api/elements-badge-timeline-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/elements-badge-timeline-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "elements-badge-timeline-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Reconstructs one person's movements through a building from their Honeywell Elements (LenelS2 Elements) badge taps.
|
|
7
|
+
Use this for incident reconstruction / "follow the badge" requests, e.g. "reconstruct Eve's movements
|
|
8
|
+
yesterday" or "where did this cardholder go".
|
|
9
|
+
|
|
10
|
+
Returns the cardholder's badge taps in CHRONOLOGICAL order (oldest first), each with:
|
|
11
|
+
- datetime / timestampMs and the area entered
|
|
12
|
+
- deviceUuid: the camera at that door
|
|
13
|
+
- clipHint (camera + start/end window) and stillHint (camera + timestamp)
|
|
14
|
+
- gapToNextSeconds: time until the next tap (a large gap = unobserved movement between doors)
|
|
15
|
+
plus a "path" array summarizing the areas traversed in order.
|
|
16
|
+
|
|
17
|
+
Resolve relative times like "yesterday" to ISO 8601 first (use the timestamp tool), then pass
|
|
18
|
+
startTime/endTime. cardholderQuery is a full-text name match; if "ambiguousCardholders" is returned the
|
|
19
|
+
query matched more than one person — ask the user which one before trusting the timeline.
|
|
20
|
+
|
|
21
|
+
IMPORTANT — to show the movement visually: for each stop (or the key transitions), call the camera-tool
|
|
22
|
+
(requestType "image", cameraUuid = stop.deviceUuid, timestamp = stop.timestampMs) for a still you can see,
|
|
23
|
+
and/or the clips-tool (requestType "createClip", using stop.clipHint) for video. Issue those per-stop
|
|
24
|
+
media calls in PARALLEL, then present the timeline as a chronological narrative.
|
|
25
|
+
`;
|
|
26
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
27
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
28
|
+
try {
|
|
29
|
+
const result = await getElementsBadgeTimeline({
|
|
30
|
+
cardholderQuery: args.cardholderQuery,
|
|
31
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
32
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
33
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
34
|
+
clipPaddingSeconds: args.clipPaddingSeconds ?? undefined,
|
|
35
|
+
limit: args.limit ?? undefined,
|
|
36
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
37
|
+
return createToolStructuredContent(result);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
41
|
+
return createToolStructuredContent({ error: message });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export function createTool(server) {
|
|
45
|
+
server.registerTool(TOOL_NAME, {
|
|
46
|
+
description: TOOL_DESCRIPTION,
|
|
47
|
+
inputSchema: TOOL_ARGS,
|
|
48
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
49
|
+
}, TOOL_HANDLER);
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getElementsLostBadgeResponse } from "../api/elements-lost-badge-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/elements-lost-badge-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "elements-lost-badge-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Lost / stolen-badge live response for Honeywell Elements (LenelS2 Elements) access. Use for "a lost badge was just used —
|
|
7
|
+
who is it and where did they go?", or to review lost/inactive-badge use over a window.
|
|
8
|
+
|
|
9
|
+
For each lost/inactive-badge use it returns:
|
|
10
|
+
- cardholderOfRecord (who the badge belongs to — may NOT be who used it) and badgeStatus
|
|
11
|
+
- the door deviceUuid + time, plus clipHint / stillHint for the door footage
|
|
12
|
+
- facesAtDoor: the face(s) captured at the door at that moment — a recognized name, or UNIDENTIFIED (an unknown
|
|
13
|
+
face on a valid badge is the strongest stolen/shared-badge signal), with a thumbnail
|
|
14
|
+
- sightings: that same face tracked across cameras, ordered in time, ending in lastKnownSighting (last-known location)
|
|
15
|
+
|
|
16
|
+
Resolve relative times (e.g. "in the last hour") to ISO 8601 first via the timestamp tool.
|
|
17
|
+
|
|
18
|
+
To present: show the door still/clip (camera-tool image / clips-tool createClip with the hints), the face at the
|
|
19
|
+
door, and the cross-camera track to last-known location. Detection + door evidence are reliable; the face track
|
|
20
|
+
depends on face-recognition coverage, so treat it as investigative, not proof.
|
|
21
|
+
`;
|
|
22
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
23
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
24
|
+
try {
|
|
25
|
+
const result = await getElementsLostBadgeResponse({
|
|
26
|
+
area: args.area ?? undefined,
|
|
27
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
28
|
+
deviceUuids: args.deviceUuids ?? undefined,
|
|
29
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
30
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
31
|
+
faceWindowSeconds: args.faceWindowSeconds ?? undefined,
|
|
32
|
+
limit: args.limit ?? undefined,
|
|
33
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
34
|
+
return createToolStructuredContent(result);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
38
|
+
return createToolStructuredContent({ error: message });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export function createTool(server) {
|
|
42
|
+
server.registerTool(TOOL_NAME, {
|
|
43
|
+
description: TOOL_DESCRIPTION,
|
|
44
|
+
inputSchema: TOOL_ARGS,
|
|
45
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
46
|
+
}, TOOL_HANDLER);
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { searchElementsEvents } from "../api/elements-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/elements-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "elements-events-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Searches Honeywell Elements (LenelS2 Elements) badge / access-control events for the organization. Use this to answer
|
|
7
|
+
"who entered WHERE and WHEN" questions, e.g. "who entered the back office yesterday".
|
|
8
|
+
|
|
9
|
+
Each returned event includes:
|
|
10
|
+
- cardholderName: the person's name
|
|
11
|
+
- deviceUuid: the camera that saw the event
|
|
12
|
+
- timestampMs / datetime: when it happened
|
|
13
|
+
- label: e.g. "Elements: Badge Authorized" (a grant) or an anomaly label
|
|
14
|
+
- badgeStatus, badgeType, areaEntering, areaExiting, entryMade, isAnomaly
|
|
15
|
+
|
|
16
|
+
Filters (all optional): area, locationUuids, deviceUuids, cardholderQuery, badgeStatus, badgeType,
|
|
17
|
+
anomalyOnly, entryMade, startTime, endTime, limit. Resolve relative times like "yesterday" to ISO 8601
|
|
18
|
+
first (use the timestamp tool), then pass startTime/endTime.
|
|
19
|
+
|
|
20
|
+
IMPORTANT — to show pictures and video of each person so the user can visually identify them: after this
|
|
21
|
+
returns, for each event (or the most relevant ones) call the camera-tool (requestType "image",
|
|
22
|
+
cameraUuid = the event's deviceUuid, timestamp = the event's time) to get a still you can see, and/or the
|
|
23
|
+
clips-tool (requestType "createClip") with a short window around the timestamp for video. Issue those
|
|
24
|
+
per-event media calls in PARALLEL.
|
|
25
|
+
`;
|
|
26
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
27
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
28
|
+
try {
|
|
29
|
+
const result = await searchElementsEvents({
|
|
30
|
+
area: args.area ?? undefined,
|
|
31
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
32
|
+
deviceUuids: args.deviceUuids ?? undefined,
|
|
33
|
+
cardholderQuery: args.cardholderQuery ?? undefined,
|
|
34
|
+
badgeStatus: args.badgeStatus ?? undefined,
|
|
35
|
+
badgeType: args.badgeType ?? undefined,
|
|
36
|
+
anomalyOnly: args.anomalyOnly ?? undefined,
|
|
37
|
+
entryMade: args.entryMade ?? undefined,
|
|
38
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
39
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
40
|
+
limit: args.limit ?? undefined,
|
|
41
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
42
|
+
return createToolStructuredContent(result);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
46
|
+
return createToolStructuredContent({ error: message });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export function createTool(server) {
|
|
50
|
+
server.registerTool(TOOL_NAME, {
|
|
51
|
+
description: TOOL_DESCRIPTION,
|
|
52
|
+
inputSchema: TOOL_ARGS,
|
|
53
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
54
|
+
}, TOOL_HANDLER);
|
|
55
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Identical arg/output shapes to the OnGuard access-anomaly tool (same generalized backend), so reuse
|
|
2
|
+
// the OnGuard Zod schemas verbatim. Re-exported to keep the elements-* module naming convention parallel.
|
|
3
|
+
export { ANOMALY_RULES, TOOL_ARGS, AnomalyFindingSchema, OUTPUT_SCHEMA, } from "./access-anomaly-tool-types.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Identical arg/output shapes to the OnGuard badge-timeline tool (same generalized backend), so reuse
|
|
2
|
+
// the OnGuard Zod schemas verbatim. Re-exported to keep the elements-* module naming convention parallel.
|
|
3
|
+
export { TOOL_ARGS, BadgeTimelineStopSchema, OUTPUT_SCHEMA, } from "./badge-timeline-tool-types.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Identical arg/output shapes to the OnGuard lost-badge tool (same generalized backend), so reuse the
|
|
2
|
+
// OnGuard Zod schemas verbatim. Re-exported to keep the elements-* module naming convention parallel.
|
|
3
|
+
export { TOOL_ARGS, LostBadgeIncidentSchema, OUTPUT_SCHEMA, } from "./lost-badge-tool-types.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The Honeywell Elements event search exposes the exact same filters and result shape as OnGuard
|
|
2
|
+
// (the backend uses one generalized DTO), so reuse the OnGuard Zod schemas verbatim rather than
|
|
3
|
+
// duplicating them. Re-exported here to keep the elements-* module naming convention parallel.
|
|
4
|
+
export { TOOL_ARGS, OnGuardEventSchema as ElementsEventSchema, OUTPUT_SCHEMA, } from "./onguard-tool-types.js";
|