ultravisor 1.0.21 → 1.0.22
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/docs/_version.json +7 -0
- package/docs/css/docuserve.css +277 -23
- package/docs/features/beacon-authentication.md +24 -31
- package/docs/features/beacon-providers.md +31 -37
- package/docs/features/beacons.md +20 -19
- package/docs/features/case-study-retold-remote.md +28 -28
- package/docs/features/llm-model-setup.md +15 -15
- package/docs/features/llm.md +29 -27
- package/docs/features/platform-cards.md +10 -10
- package/docs/features/reachability-matrix.md +12 -12
- package/docs/features/tasks-content-system.md +32 -32
- package/docs/features/tasks-data-transform.md +64 -64
- package/docs/features/tasks-extension.md +14 -14
- package/docs/features/tasks-file-system.md +94 -94
- package/docs/features/tasks-flow-control.md +38 -38
- package/docs/features/tasks-http-client.md +40 -40
- package/docs/features/tasks-llm.md +58 -58
- package/docs/features/tasks-meadow-api.md +50 -50
- package/docs/features/tasks-user-interaction.md +12 -12
- package/docs/features/tasks.md +20 -20
- package/docs/features/universal-addressing.md +12 -12
- package/docs/index.html +2 -2
- package/docs/retold-catalog.json +30 -1
- package/docs/retold-keyword-index.json +15389 -12741
- package/package.json +4 -3
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +39 -0
- package/source/services/Ultravisor-OperationAuditor.cjs +471 -0
- package/source/web_server/Ultravisor-API-Server.cjs +54 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultravisor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "Cyclic process execution with ai integration.",
|
|
5
5
|
"main": "source/Ultravisor.cjs",
|
|
6
6
|
"bin": {
|
|
@@ -31,15 +31,16 @@
|
|
|
31
31
|
"orator": "^6.0.4",
|
|
32
32
|
"orator-authentication": "^1.0.0",
|
|
33
33
|
"orator-serviceserver-restify": "^2.0.10",
|
|
34
|
-
"pict": "^1.0.
|
|
34
|
+
"pict": "^1.0.363",
|
|
35
35
|
"pict-service-commandlineutility": "^1.0.19",
|
|
36
36
|
"pict-serviceproviderbase": "^1.0.4",
|
|
37
37
|
"ultravisor-beacon": "^0.0.11",
|
|
38
38
|
"ws": "^8.20.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
+
"pict-docuserve": "^0.1.5",
|
|
41
42
|
"puppeteer": "^24.40.0",
|
|
42
|
-
"quackage": "^1.0
|
|
43
|
+
"quackage": "^1.1.0"
|
|
43
44
|
},
|
|
44
45
|
"mocha": {
|
|
45
46
|
"diff": true,
|
|
@@ -1923,6 +1923,13 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1923
1923
|
|
|
1924
1924
|
/**
|
|
1925
1925
|
* Remove a work item hash from a Beacon's CurrentWorkItems array.
|
|
1926
|
+
*
|
|
1927
|
+
* After freeing the slot, any still-Pending work items that match
|
|
1928
|
+
* the Beacon's capabilities get a fresh push attempt. This is
|
|
1929
|
+
* important for WebSocket beacons: they only receive work via
|
|
1930
|
+
* `_tryPushToWebSocketBeacon`, and without this re-dispatch a
|
|
1931
|
+
* parallel work item enqueued while the beacon was full would sit
|
|
1932
|
+
* Pending forever (nobody polls for WebSocket beacons).
|
|
1926
1933
|
*/
|
|
1927
1934
|
_removeWorkItemFromBeacon(pBeaconID, pWorkItemHash)
|
|
1928
1935
|
{
|
|
@@ -1949,6 +1956,38 @@ class UltravisorBeaconCoordinator extends libPictService
|
|
|
1949
1956
|
{
|
|
1950
1957
|
tmpBeacon.Status = 'Online';
|
|
1951
1958
|
}
|
|
1959
|
+
|
|
1960
|
+
// Re-attempt dispatch of any pending work items. The beacon may
|
|
1961
|
+
// now have capacity for items that were enqueued while it was
|
|
1962
|
+
// busy. _tryPushToWebSocketBeacon iterates all registered
|
|
1963
|
+
// beacons so this also covers the case where a different beacon
|
|
1964
|
+
// came online mid-run.
|
|
1965
|
+
this._dispatchPendingWorkItems();
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* Walk the work queue and attempt to push any Pending,
|
|
1970
|
+
* unassigned work items via the WebSocket dispatch path.
|
|
1971
|
+
*
|
|
1972
|
+
* Safe to call any time — the push helper re-checks beacon
|
|
1973
|
+
* capacity, capability match, and state. This is the single
|
|
1974
|
+
* entry point for "something changed that might let a parked
|
|
1975
|
+
* work item move" (slot freed, new beacon registered, etc.).
|
|
1976
|
+
*/
|
|
1977
|
+
_dispatchPendingWorkItems()
|
|
1978
|
+
{
|
|
1979
|
+
// No WebSocket dispatch handler means nothing to push to.
|
|
1980
|
+
if (!this._WorkItemPushHandler) return;
|
|
1981
|
+
|
|
1982
|
+
let tmpHashes = Object.keys(this._WorkQueue);
|
|
1983
|
+
for (let i = 0; i < tmpHashes.length; i++)
|
|
1984
|
+
{
|
|
1985
|
+
let tmpWI = this._WorkQueue[tmpHashes[i]];
|
|
1986
|
+
if (!tmpWI) continue;
|
|
1987
|
+
if (tmpWI.Status !== 'Pending') continue;
|
|
1988
|
+
if (tmpWI.AssignedBeaconID) continue; // affinity-assigned, leave it
|
|
1989
|
+
this._tryPushToWebSocketBeacon(tmpWI);
|
|
1990
|
+
}
|
|
1952
1991
|
}
|
|
1953
1992
|
|
|
1954
1993
|
/**
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
const libPictService = require('pict-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ultravisor Operation Auditor
|
|
5
|
+
*
|
|
6
|
+
* Cross-references beacon-dispatch nodes in registered operations against
|
|
7
|
+
* the beacon action catalog (SettingsSchema) to find port-mapping bugs.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: Ultravisor resolves state-connection values into a
|
|
10
|
+
* worker's Settings dict by extracting the port name from the substring
|
|
11
|
+
* AFTER the last -ei-/-eo-/-si-/-so- in the port hash. That extracted
|
|
12
|
+
* name becomes the settings key the worker receives. If the port hash
|
|
13
|
+
* doesn't end with a key the worker actually reads, the value is silently
|
|
14
|
+
* dropped and the worker falls back to its default — producing
|
|
15
|
+
* confusing-but-non-fatal bugs that only surface as "wrong output".
|
|
16
|
+
*
|
|
17
|
+
* This auditor walks every registered operation and flags:
|
|
18
|
+
*
|
|
19
|
+
* - TGT_PORT_NOT_IN_SCHEMA — a state connection's target port hash
|
|
20
|
+
* extracts to a name that is not in the target action's
|
|
21
|
+
* SettingsSchema. The value will be sent to the worker under a key
|
|
22
|
+
* the worker ignores.
|
|
23
|
+
*
|
|
24
|
+
* - UNKNOWN_DATA_KEY — a beacon-dispatch node's static Data object
|
|
25
|
+
* contains a key that is not in the target action's SettingsSchema.
|
|
26
|
+
* Same problem: the worker never reads it.
|
|
27
|
+
*
|
|
28
|
+
* - VALUE_INPUT_SRC_MISMATCH — a state connection whose source is a
|
|
29
|
+
* value-input node has a source port hash that does not extract to
|
|
30
|
+
* "InputValue" (which is the fixed output key for value-input).
|
|
31
|
+
*
|
|
32
|
+
* - SRC_PORT_NOT_IN_SCHEMA (soft) — only reported when the source
|
|
33
|
+
* action publishes an OutputsSchema. Since the standard beacon
|
|
34
|
+
* registration only exposes input schemas, this is skipped unless
|
|
35
|
+
* an OutputsSchema has been added.
|
|
36
|
+
*
|
|
37
|
+
* Source-side mismatches (e.g. reading `output_file` when the worker
|
|
38
|
+
* returns `video_file`) cannot be detected from the action catalog alone
|
|
39
|
+
* because beacons do not publish output schemas. Runtime telemetry or a
|
|
40
|
+
* caller-supplied map is required for full source-side coverage.
|
|
41
|
+
*
|
|
42
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
43
|
+
*/
|
|
44
|
+
class UltravisorOperationAuditor extends libPictService
|
|
45
|
+
{
|
|
46
|
+
constructor(pPict, pOptions, pServiceHash)
|
|
47
|
+
{
|
|
48
|
+
super(pPict, pOptions, pServiceHash);
|
|
49
|
+
|
|
50
|
+
this.serviceType = 'UltravisorOperationAuditor';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get a named Fable service or null if not registered.
|
|
55
|
+
*/
|
|
56
|
+
_getService(pTypeName)
|
|
57
|
+
{
|
|
58
|
+
return this.fable.servicesMap[pTypeName]
|
|
59
|
+
? Object.values(this.fable.servicesMap[pTypeName])[0]
|
|
60
|
+
: null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract the logical port name from a port hash, mirroring
|
|
65
|
+
* Ultravisor-ExecutionEngine._extractPortName so the auditor reports
|
|
66
|
+
* what the real execution engine would actually resolve.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} pPortHash - The port hash string.
|
|
69
|
+
* @param {object} [pPortLabelMap] - Optional hash -> label fallback map.
|
|
70
|
+
* @returns {string} The extracted port name, or '' if input is invalid.
|
|
71
|
+
*/
|
|
72
|
+
extractPortName(pPortHash, pPortLabelMap)
|
|
73
|
+
{
|
|
74
|
+
if (!pPortHash || typeof(pPortHash) !== 'string')
|
|
75
|
+
{
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let tmpPrefixes = ['-ei-', '-eo-', '-si-', '-so-'];
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < tmpPrefixes.length; i++)
|
|
82
|
+
{
|
|
83
|
+
let tmpIndex = pPortHash.lastIndexOf(tmpPrefixes[i]);
|
|
84
|
+
if (tmpIndex > -1)
|
|
85
|
+
{
|
|
86
|
+
return pPortHash.substring(tmpIndex + tmpPrefixes[i].length);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (pPortLabelMap && pPortLabelMap[pPortHash])
|
|
91
|
+
{
|
|
92
|
+
return pPortLabelMap[pPortHash];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return pPortHash;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a fallback hash -> label lookup from a graph.
|
|
100
|
+
*/
|
|
101
|
+
_buildPortLabelMap(pGraph)
|
|
102
|
+
{
|
|
103
|
+
let tmpMap = {};
|
|
104
|
+
let tmpNodes = (pGraph && pGraph.Nodes) || [];
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
107
|
+
{
|
|
108
|
+
let tmpPorts = tmpNodes[i].Ports || [];
|
|
109
|
+
for (let j = 0; j < tmpPorts.length; j++)
|
|
110
|
+
{
|
|
111
|
+
tmpMap[tmpPorts[j].Hash] = tmpPorts[j].Label || tmpPorts[j].Hash;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return tmpMap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Index the beacon action catalog by "Capability/Action" for lookup.
|
|
120
|
+
* Returns a map where each value is the set of SettingsSchema field
|
|
121
|
+
* names the worker accepts.
|
|
122
|
+
*
|
|
123
|
+
* @returns {object} { "Capability/Action": Set<string> }
|
|
124
|
+
*/
|
|
125
|
+
_buildActionSchemaIndex()
|
|
126
|
+
{
|
|
127
|
+
let tmpIndex = {};
|
|
128
|
+
let tmpCoordinator = this._getService('UltravisorBeaconCoordinator');
|
|
129
|
+
|
|
130
|
+
if (!tmpCoordinator || typeof(tmpCoordinator.getActionCatalog) !== 'function')
|
|
131
|
+
{
|
|
132
|
+
return tmpIndex;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let tmpCatalog = tmpCoordinator.getActionCatalog();
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < tmpCatalog.length; i++)
|
|
138
|
+
{
|
|
139
|
+
let tmpEntry = tmpCatalog[i];
|
|
140
|
+
let tmpKey = `${tmpEntry.Capability}/${tmpEntry.Action}`;
|
|
141
|
+
let tmpReads = new Set();
|
|
142
|
+
|
|
143
|
+
let tmpSchema = tmpEntry.SettingsSchema || [];
|
|
144
|
+
for (let j = 0; j < tmpSchema.length; j++)
|
|
145
|
+
{
|
|
146
|
+
let tmpField = tmpSchema[j];
|
|
147
|
+
if (tmpField && tmpField.Name)
|
|
148
|
+
{
|
|
149
|
+
tmpReads.add(tmpField.Name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
tmpIndex[tmpKey] = tmpReads;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return tmpIndex;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Meta keys that live inside a beacon-dispatch node's Data object but
|
|
161
|
+
* are NOT forwarded to the worker under their own name. Two categories:
|
|
162
|
+
*
|
|
163
|
+
* 1. Ultravisor-level meta consumed by Extension task config itself.
|
|
164
|
+
* See Ultravisor-TaskConfigs-Extension.cjs. These identify
|
|
165
|
+
* capability/action/timeout/affinity for the beacon dispatch
|
|
166
|
+
* machinery before anything hits a worker.
|
|
167
|
+
*
|
|
168
|
+
* 2. Beacon-host meta consumed by retold-labs (or a similar beacon
|
|
169
|
+
* provider) before the work item is sent to the Python worker.
|
|
170
|
+
* The current example is `venv_path`, which retold-labs'
|
|
171
|
+
* LibraryScanner injects into library-generated operations so
|
|
172
|
+
* BeaconSetup._executeWorker can resolve a library-declared env
|
|
173
|
+
* to a Python interpreter. BeaconSetup strips the key before
|
|
174
|
+
* sending settings to the worker, so the worker never sees it
|
|
175
|
+
* and its SettingsSchema never declares it.
|
|
176
|
+
*
|
|
177
|
+
* Auditor treats both categories as meta so they don't flag as
|
|
178
|
+
* UNKNOWN_DATA_KEY when present on a beacon-dispatch node.
|
|
179
|
+
*/
|
|
180
|
+
_getDispatchMetaKeys()
|
|
181
|
+
{
|
|
182
|
+
return new Set([
|
|
183
|
+
// Ultravisor-level meta (Extension task config)
|
|
184
|
+
'RemoteCapability',
|
|
185
|
+
'RemoteAction',
|
|
186
|
+
'AffinityKey',
|
|
187
|
+
'TimeoutMs',
|
|
188
|
+
'PromptMessage',
|
|
189
|
+
'OutputAddress',
|
|
190
|
+
'InputSchema',
|
|
191
|
+
|
|
192
|
+
// Beacon-host meta (consumed by retold-labs before worker dispatch)
|
|
193
|
+
'venv_path'
|
|
194
|
+
]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Audit a single operation definition.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} pOperation - An operation definition (with .Graph).
|
|
201
|
+
* @param {object} pActionIndex - From _buildActionSchemaIndex().
|
|
202
|
+
* @returns {Array<object>} List of issue objects.
|
|
203
|
+
*/
|
|
204
|
+
auditOperation(pOperation, pActionIndex)
|
|
205
|
+
{
|
|
206
|
+
let tmpIssues = [];
|
|
207
|
+
|
|
208
|
+
if (!pOperation || !pOperation.Graph)
|
|
209
|
+
{
|
|
210
|
+
return tmpIssues;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let tmpGraph = pOperation.Graph;
|
|
214
|
+
let tmpNodes = tmpGraph.Nodes || [];
|
|
215
|
+
let tmpConnections = tmpGraph.Connections || [];
|
|
216
|
+
let tmpPortLabelMap = this._buildPortLabelMap(tmpGraph);
|
|
217
|
+
let tmpMetaKeys = this._getDispatchMetaKeys();
|
|
218
|
+
|
|
219
|
+
// Index nodes and build a dispatch-node map with action + reads
|
|
220
|
+
let tmpNodesByHash = {};
|
|
221
|
+
let tmpDispatchNodes = {};
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
224
|
+
{
|
|
225
|
+
let tmpNode = tmpNodes[i];
|
|
226
|
+
tmpNodesByHash[tmpNode.Hash] = tmpNode;
|
|
227
|
+
|
|
228
|
+
if (tmpNode.Type === 'beacon-dispatch')
|
|
229
|
+
{
|
|
230
|
+
let tmpData = tmpNode.Data || {};
|
|
231
|
+
let tmpCap = tmpData.RemoteCapability || '';
|
|
232
|
+
let tmpAction = tmpData.RemoteAction || '';
|
|
233
|
+
let tmpKey = `${tmpCap}/${tmpAction}`;
|
|
234
|
+
let tmpReads = pActionIndex[tmpKey] || null;
|
|
235
|
+
|
|
236
|
+
tmpDispatchNodes[tmpNode.Hash] = {
|
|
237
|
+
Node: tmpNode,
|
|
238
|
+
Capability: tmpCap,
|
|
239
|
+
Action: tmpAction,
|
|
240
|
+
CatalogKey: tmpKey,
|
|
241
|
+
Reads: tmpReads,
|
|
242
|
+
HasSchema: !!tmpReads
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// If the action isn't in the catalog, the auditor has no ground
|
|
246
|
+
// truth for this node — emit a skip notice so callers understand
|
|
247
|
+
// why nothing else was flagged here.
|
|
248
|
+
if (!tmpReads)
|
|
249
|
+
{
|
|
250
|
+
tmpIssues.push(
|
|
251
|
+
{
|
|
252
|
+
Kind: 'ACTION_NOT_IN_CATALOG',
|
|
253
|
+
Severity: 'info',
|
|
254
|
+
Node: tmpNode.Hash,
|
|
255
|
+
NodeTitle: tmpNode.Title || '',
|
|
256
|
+
CatalogKey: tmpKey,
|
|
257
|
+
Detail: `Capability/Action [${tmpKey}] is not in the beacon action catalog; no schema is available to audit this node against. Connect the providing beacon to populate the catalog, then re-run.`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
{
|
|
262
|
+
// Check static Data keys against the worker's reads
|
|
263
|
+
let tmpDataKeys = Object.keys(tmpData);
|
|
264
|
+
for (let k = 0; k < tmpDataKeys.length; k++)
|
|
265
|
+
{
|
|
266
|
+
let tmpKeyName = tmpDataKeys[k];
|
|
267
|
+
if (tmpMetaKeys.has(tmpKeyName))
|
|
268
|
+
{
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!tmpReads.has(tmpKeyName))
|
|
272
|
+
{
|
|
273
|
+
tmpIssues.push(
|
|
274
|
+
{
|
|
275
|
+
Kind: 'UNKNOWN_DATA_KEY',
|
|
276
|
+
Severity: 'warning',
|
|
277
|
+
Node: tmpNode.Hash,
|
|
278
|
+
NodeTitle: tmpNode.Title || '',
|
|
279
|
+
CatalogKey: tmpKey,
|
|
280
|
+
Key: tmpKeyName,
|
|
281
|
+
Detail: `Node Data contains '${tmpKeyName}' which ${tmpKey} does not declare in its SettingsSchema; the worker will ignore it.`
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check every state connection
|
|
290
|
+
for (let i = 0; i < tmpConnections.length; i++)
|
|
291
|
+
{
|
|
292
|
+
let tmpConn = tmpConnections[i];
|
|
293
|
+
if (tmpConn.ConnectionType !== 'state')
|
|
294
|
+
{
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let tmpSrcNodeHash = tmpConn.SourceNodeHash || '';
|
|
299
|
+
let tmpTgtNodeHash = tmpConn.TargetNodeHash || '';
|
|
300
|
+
let tmpSrcPortHash = tmpConn.SourcePortHash || '';
|
|
301
|
+
let tmpTgtPortHash = tmpConn.TargetPortHash || '';
|
|
302
|
+
|
|
303
|
+
let tmpSrcName = this.extractPortName(tmpSrcPortHash, tmpPortLabelMap);
|
|
304
|
+
let tmpTgtName = this.extractPortName(tmpTgtPortHash, tmpPortLabelMap);
|
|
305
|
+
let tmpConnHash = tmpConn.Hash || '(unnamed)';
|
|
306
|
+
|
|
307
|
+
// Target side: resolved name must be in the target action's SettingsSchema
|
|
308
|
+
let tmpTgtDispatch = tmpDispatchNodes[tmpTgtNodeHash];
|
|
309
|
+
if (tmpTgtDispatch && tmpTgtDispatch.HasSchema)
|
|
310
|
+
{
|
|
311
|
+
if (!tmpTgtDispatch.Reads.has(tmpTgtName))
|
|
312
|
+
{
|
|
313
|
+
tmpIssues.push(
|
|
314
|
+
{
|
|
315
|
+
Kind: 'TGT_PORT_NOT_IN_SCHEMA',
|
|
316
|
+
Severity: 'error',
|
|
317
|
+
Connection: tmpConnHash,
|
|
318
|
+
TargetNode: tmpTgtNodeHash,
|
|
319
|
+
CatalogKey: tmpTgtDispatch.CatalogKey,
|
|
320
|
+
TargetPort: tmpTgtPortHash,
|
|
321
|
+
ExtractedName: tmpTgtName,
|
|
322
|
+
Detail: `Target port hash extracts to '${tmpTgtName}' but ${tmpTgtDispatch.CatalogKey} does not declare that field in its SettingsSchema; the state value will be delivered under a key the worker ignores.`
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Source side: value-input nodes always emit under "InputValue"
|
|
328
|
+
let tmpSrcNode = tmpNodesByHash[tmpSrcNodeHash];
|
|
329
|
+
if (tmpSrcNode && tmpSrcNode.Type === 'value-input')
|
|
330
|
+
{
|
|
331
|
+
if (tmpSrcName !== 'InputValue')
|
|
332
|
+
{
|
|
333
|
+
tmpIssues.push(
|
|
334
|
+
{
|
|
335
|
+
Kind: 'VALUE_INPUT_SRC_MISMATCH',
|
|
336
|
+
Severity: 'error',
|
|
337
|
+
Connection: tmpConnHash,
|
|
338
|
+
SourceNode: tmpSrcNodeHash,
|
|
339
|
+
SourcePort: tmpSrcPortHash,
|
|
340
|
+
ExtractedName: tmpSrcName,
|
|
341
|
+
Detail: `Source is a value-input node; source port should extract to 'InputValue' but extracts to '${tmpSrcName}'. The connection will read undefined.`
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return tmpIssues;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Audit every operation currently registered in HypervisorState.
|
|
352
|
+
*
|
|
353
|
+
* @param {function} fCallback - (pError, pReport)
|
|
354
|
+
* where pReport = {
|
|
355
|
+
* AuditedAt: ISO timestamp,
|
|
356
|
+
* OperationCount: number,
|
|
357
|
+
* IssueCount: number,
|
|
358
|
+
* ActionCatalogSize: number,
|
|
359
|
+
* Operations: [ { Hash, Name, IssueCount, Issues } ],
|
|
360
|
+
* WorstByKind: { KIND: count }
|
|
361
|
+
* }
|
|
362
|
+
*/
|
|
363
|
+
auditAll(fCallback)
|
|
364
|
+
{
|
|
365
|
+
let tmpState = this._getService('UltravisorHypervisorState');
|
|
366
|
+
if (!tmpState)
|
|
367
|
+
{
|
|
368
|
+
return fCallback(new Error('UltravisorHypervisorState service not available.'));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let tmpActionIndex = this._buildActionSchemaIndex();
|
|
372
|
+
let tmpActionCatalogSize = Object.keys(tmpActionIndex).length;
|
|
373
|
+
|
|
374
|
+
tmpState.getOperationList(
|
|
375
|
+
(pError, pOperations) =>
|
|
376
|
+
{
|
|
377
|
+
if (pError)
|
|
378
|
+
{
|
|
379
|
+
return fCallback(pError);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let tmpOperations = [];
|
|
383
|
+
let tmpTotalIssues = 0;
|
|
384
|
+
let tmpWorstByKind = {};
|
|
385
|
+
|
|
386
|
+
for (let i = 0; i < pOperations.length; i++)
|
|
387
|
+
{
|
|
388
|
+
let tmpOp = pOperations[i];
|
|
389
|
+
let tmpIssues = this.auditOperation(tmpOp, tmpActionIndex);
|
|
390
|
+
tmpTotalIssues += tmpIssues.length;
|
|
391
|
+
|
|
392
|
+
for (let j = 0; j < tmpIssues.length; j++)
|
|
393
|
+
{
|
|
394
|
+
let tmpKind = tmpIssues[j].Kind;
|
|
395
|
+
tmpWorstByKind[tmpKind] = (tmpWorstByKind[tmpKind] || 0) + 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
tmpOperations.push(
|
|
399
|
+
{
|
|
400
|
+
Hash: tmpOp.Hash,
|
|
401
|
+
Name: tmpOp.Name || '',
|
|
402
|
+
IssueCount: tmpIssues.length,
|
|
403
|
+
Issues: tmpIssues
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Sort: worst first, then by name
|
|
408
|
+
tmpOperations.sort(
|
|
409
|
+
(a, b) =>
|
|
410
|
+
{
|
|
411
|
+
if (b.IssueCount !== a.IssueCount) return b.IssueCount - a.IssueCount;
|
|
412
|
+
return (a.Name || a.Hash).localeCompare(b.Name || b.Hash);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
let tmpReport =
|
|
416
|
+
{
|
|
417
|
+
AuditedAt: new Date().toISOString(),
|
|
418
|
+
OperationCount: pOperations.length,
|
|
419
|
+
IssueCount: tmpTotalIssues,
|
|
420
|
+
ActionCatalogSize: tmpActionCatalogSize,
|
|
421
|
+
WorstByKind: tmpWorstByKind,
|
|
422
|
+
Operations: tmpOperations
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
this.log.info(`OperationAuditor: scanned ${pOperations.length} operations, found ${tmpTotalIssues} issue(s).`);
|
|
426
|
+
|
|
427
|
+
return fCallback(null, tmpReport);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Audit a single operation by hash.
|
|
433
|
+
*
|
|
434
|
+
* @param {string} pHash - Operation hash.
|
|
435
|
+
* @param {function} fCallback - (pError, pResult)
|
|
436
|
+
*/
|
|
437
|
+
auditByHash(pHash, fCallback)
|
|
438
|
+
{
|
|
439
|
+
let tmpState = this._getService('UltravisorHypervisorState');
|
|
440
|
+
if (!tmpState)
|
|
441
|
+
{
|
|
442
|
+
return fCallback(new Error('UltravisorHypervisorState service not available.'));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
tmpState.getOperation(pHash,
|
|
446
|
+
(pError, pOperation) =>
|
|
447
|
+
{
|
|
448
|
+
if (pError)
|
|
449
|
+
{
|
|
450
|
+
return fCallback(pError);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let tmpActionIndex = this._buildActionSchemaIndex();
|
|
454
|
+
let tmpIssues = this.auditOperation(pOperation, tmpActionIndex);
|
|
455
|
+
|
|
456
|
+
return fCallback(null,
|
|
457
|
+
{
|
|
458
|
+
AuditedAt: new Date().toISOString(),
|
|
459
|
+
Hash: pOperation.Hash,
|
|
460
|
+
Name: pOperation.Name || '',
|
|
461
|
+
IssueCount: tmpIssues.length,
|
|
462
|
+
ActionCatalogSize: Object.keys(tmpActionIndex).length,
|
|
463
|
+
Issues: tmpIssues
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
module.exports = UltravisorOperationAuditor;
|
|
470
|
+
module.exports.serviceType = 'UltravisorOperationAuditor';
|
|
471
|
+
module.exports.default_configuration = {};
|
|
@@ -253,6 +253,60 @@ class UltravisorAPIServer extends libPictService
|
|
|
253
253
|
}.bind(this)
|
|
254
254
|
);
|
|
255
255
|
|
|
256
|
+
// --- Operation Audit ---
|
|
257
|
+
// Static port-mapping audit across all registered operations.
|
|
258
|
+
// Cross-references beacon-dispatch nodes' state connections and
|
|
259
|
+
// Data keys against the beacon action catalog's SettingsSchema.
|
|
260
|
+
this._OratorServer.get
|
|
261
|
+
(
|
|
262
|
+
'/OperationAudit',
|
|
263
|
+
function (pRequest, pResponse, fNext)
|
|
264
|
+
{
|
|
265
|
+
let tmpAuditor = this._getService('UltravisorOperationAuditor');
|
|
266
|
+
if (!tmpAuditor)
|
|
267
|
+
{
|
|
268
|
+
pResponse.send(503, { Error: 'UltravisorOperationAuditor service not available.' });
|
|
269
|
+
return fNext();
|
|
270
|
+
}
|
|
271
|
+
tmpAuditor.auditAll(
|
|
272
|
+
function (pError, pReport)
|
|
273
|
+
{
|
|
274
|
+
if (pError)
|
|
275
|
+
{
|
|
276
|
+
pResponse.send(500, { Error: pError.message });
|
|
277
|
+
return fNext();
|
|
278
|
+
}
|
|
279
|
+
pResponse.send(pReport);
|
|
280
|
+
return fNext();
|
|
281
|
+
});
|
|
282
|
+
}.bind(this)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
this._OratorServer.get
|
|
286
|
+
(
|
|
287
|
+
'/OperationAudit/:Hash',
|
|
288
|
+
function (pRequest, pResponse, fNext)
|
|
289
|
+
{
|
|
290
|
+
let tmpAuditor = this._getService('UltravisorOperationAuditor');
|
|
291
|
+
if (!tmpAuditor)
|
|
292
|
+
{
|
|
293
|
+
pResponse.send(503, { Error: 'UltravisorOperationAuditor service not available.' });
|
|
294
|
+
return fNext();
|
|
295
|
+
}
|
|
296
|
+
tmpAuditor.auditByHash(pRequest.params.Hash,
|
|
297
|
+
function (pError, pResult)
|
|
298
|
+
{
|
|
299
|
+
if (pError)
|
|
300
|
+
{
|
|
301
|
+
pResponse.send(404, { Error: pError.message });
|
|
302
|
+
return fNext();
|
|
303
|
+
}
|
|
304
|
+
pResponse.send(pResult);
|
|
305
|
+
return fNext();
|
|
306
|
+
});
|
|
307
|
+
}.bind(this)
|
|
308
|
+
);
|
|
309
|
+
|
|
256
310
|
this._OratorServer.post
|
|
257
311
|
(
|
|
258
312
|
'/Operation',
|