@nullbridge/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -0
- package/package.json +25 -0
- package/src/agents.js +192 -0
- package/src/anomaly.js +148 -0
- package/src/audit.js +112 -0
- package/src/credentials.js +197 -0
- package/src/http.js +61 -0
- package/src/index.d.ts +136 -0
- package/src/index.js +152 -0
- package/src/license.js +100 -0
- package/src/siem.js +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# @nullbridge/sdk
|
|
2
|
+
|
|
3
|
+
**AI Agent Identity Governance for Node.js**
|
|
4
|
+
|
|
5
|
+
NullBridge gives your AI agents a cryptographic identity, secure credential vault, continuous audit trail, and behavioral anomaly detection — in three lines of code.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @nullbridge/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const { NullBridge } = require('@nullbridge/sdk');
|
|
21
|
+
|
|
22
|
+
const nb = new NullBridge({
|
|
23
|
+
licenseKey: process.env.NULLBRIDGE_LICENSE_KEY,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await nb.init();
|
|
27
|
+
|
|
28
|
+
const agent = await nb.agents.register({
|
|
29
|
+
name: 'claims-processor',
|
|
30
|
+
type: 'llm',
|
|
31
|
+
model: 'gpt-4o',
|
|
32
|
+
scopes: ['read:claims', 'write:reports'],
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's it. NullBridge validates your license on startup, registers your agent with a cryptographic identity, and begins monitoring automatically.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
const nb = new NullBridge({
|
|
44
|
+
licenseKey: process.env.NULLBRIDGE_LICENSE_KEY, // required
|
|
45
|
+
serverUrl: 'https://nullbridge-license-server-production.up.railway.app', // default
|
|
46
|
+
apiUrl: 'https://api.nullbridge.ai', // default
|
|
47
|
+
skipLicense: false, // set true for local development only
|
|
48
|
+
autoShutdown: true, // shut down process if license is revoked
|
|
49
|
+
checkInterval: 86400000, // license check interval in ms (default: 24 hours)
|
|
50
|
+
debug: false, // enable verbose logging
|
|
51
|
+
siem: {
|
|
52
|
+
endpoint: 'https://your-siem.com/ingest', // optional
|
|
53
|
+
format: 'json', // 'json' | 'cef' | 'leef'
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Environment Variables
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
NULLBRIDGE_LICENSE_KEY=NB-XXXX-XXXX-XXXX-XXXX
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Contact brian@nullbridge.ai to obtain a license key.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### `nb.init()`
|
|
73
|
+
Initialize NullBridge. Call once on application startup before serving traffic.
|
|
74
|
+
Validates license and starts background services.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
await nb.init();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### `nb.agents`
|
|
83
|
+
|
|
84
|
+
#### `nb.agents.register(options)`
|
|
85
|
+
Register an AI agent with NullBridge.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const agent = await nb.agents.register({
|
|
89
|
+
name: 'fraud-detector', // required — human readable name
|
|
90
|
+
type: 'llm', // required — 'llm' | 'rpa' | 'workflow' | 'custom'
|
|
91
|
+
model: 'claude-3-5-sonnet', // optional
|
|
92
|
+
scopes: ['read:transactions', 'write:alerts'], // optional
|
|
93
|
+
metadata: { team: 'risk' }, // optional
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `agent.logAction(action, details)`
|
|
98
|
+
Log an action this agent performed.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
await agent.logAction('analyze_transaction', { txId: 'TX-1234', amount: 5000 });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `agent.hasScope(scope)`
|
|
105
|
+
Check if agent has a specific permission.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
if (!agent.hasScope('write:alerts')) throw new Error('Not authorized');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### `agent.deregister()`
|
|
112
|
+
Deregister agent and revoke its identity.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### `nb.credentials`
|
|
117
|
+
|
|
118
|
+
#### `nb.credentials.store(options)`
|
|
119
|
+
Store a credential securely (AES-256-GCM encrypted).
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const credId = await nb.credentials.store({
|
|
123
|
+
agentId: agent.id,
|
|
124
|
+
name: 'openai-api-key',
|
|
125
|
+
value: process.env.OPENAI_API_KEY,
|
|
126
|
+
type: 'api_key', // 'api_key' | 'oauth_token' | 'password' | 'certificate'
|
|
127
|
+
ttl: 86400, // optional: expire after 24 hours
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### `nb.credentials.get(credId)`
|
|
132
|
+
Retrieve a credential value.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const apiKey = await nb.credentials.get(credId);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### `nb.credentials.rotate(credId, newValue)`
|
|
139
|
+
Rotate a credential and log the rotation in audit trail.
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
await nb.credentials.rotate(credId, newApiKey);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `nb.credentials.revoke(credId)`
|
|
146
|
+
Permanently revoke a credential.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### `nb.audit`
|
|
151
|
+
|
|
152
|
+
#### `nb.audit.log(event)`
|
|
153
|
+
Log an agent action to the audit trail.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
nb.audit.log({
|
|
157
|
+
agentId: agent.id,
|
|
158
|
+
agentName: agent.name,
|
|
159
|
+
action: 'process_claim', // required
|
|
160
|
+
resource: 'claim:CLM-20240001', // optional
|
|
161
|
+
outcome: 'success', // 'success' | 'failure' | 'blocked'
|
|
162
|
+
details: { amount: 15000 }, // optional
|
|
163
|
+
ip: req.ip, // optional
|
|
164
|
+
duration: 1240, // optional: ms
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### `nb.audit.logViolation(agentId, agentName, action, reason)`
|
|
169
|
+
Log a policy violation (outcome is automatically set to 'blocked').
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
nb.audit.logViolation(agent.id, agent.name, 'delete_record', 'scope_denied');
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### `nb.anomaly`
|
|
178
|
+
|
|
179
|
+
#### `nb.anomaly.record(agentId, metric, value)`
|
|
180
|
+
Record a behavioral metric. NullBridge builds a statistical baseline and alerts on deviations.
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
nb.anomaly.record(agent.id, 'api_calls_per_minute', 14);
|
|
184
|
+
nb.anomaly.record(agent.id, 'tokens_used', 3200);
|
|
185
|
+
nb.anomaly.record(agent.id, 'unique_endpoints', 3);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### `nb.anomaly.onAlert(handler)`
|
|
189
|
+
Register a callback for anomaly alerts.
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
nb.anomaly.onAlert(alert => {
|
|
193
|
+
console.error(`Anomaly: ${alert.agentId} — ${alert.metric} (z=${alert.zScore})`);
|
|
194
|
+
// Send to PagerDuty, Slack, etc.
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### `nb.anomaly.check(agentId, metric, value)`
|
|
199
|
+
Check if a value is anomalous without recording it.
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
const { anomalous, zScore } = nb.anomaly.check(agent.id, 'api_calls', 500);
|
|
203
|
+
if (anomalous) console.warn('Unusual API call volume');
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### `nb.shutdown()`
|
|
209
|
+
Gracefully shut down — flushes audit logs and stops background services.
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
process.on('SIGTERM', async () => {
|
|
213
|
+
await nb.shutdown();
|
|
214
|
+
process.exit(0);
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
This software is proprietary and confidential. Use requires a valid NullBridge license key.
|
|
223
|
+
Contact brian@nullbridge.ai for licensing.
|
|
224
|
+
|
|
225
|
+
**NullBridge Technologies**
|
|
226
|
+
brian@nullbridge.ai | nullbridge.ai
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
CONFIDENTIALITY & IP NOTICE: This software and documentation contain proprietary and confidential
|
|
231
|
+
information belonging to NullBridge Technologies. Protected under applicable intellectual property
|
|
232
|
+
laws. Unauthorized use, disclosure, or distribution is strictly prohibited.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nullbridge/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "NullBridge AI Agent Identity Governance SDK",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"author": "NullBridge Technologies <brian@nullbridge.ai>",
|
|
9
|
+
"homepage": "https://nullbridge.ai",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/bwes03-afk/nullbridge-sdk"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai", "agents", "identity", "governance", "iam",
|
|
16
|
+
"security", "audit", "nullbridge", "license"
|
|
17
|
+
],
|
|
18
|
+
"engines": { "node": ">=16.0.0" },
|
|
19
|
+
"files": ["src/", "README.md", "LICENSE"],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node examples/basic.js",
|
|
22
|
+
"prepublishOnly": "node -e \"console.log('Publishing @nullbridge/sdk...')\""
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {}
|
|
25
|
+
}
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AgentRegistry — register, manage, and monitor AI agents
|
|
7
|
+
*/
|
|
8
|
+
class AgentRegistry {
|
|
9
|
+
constructor(config, http) {
|
|
10
|
+
this._config = config;
|
|
11
|
+
this._http = http;
|
|
12
|
+
this._agents = new Map(); // local cache
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a new AI agent with NullBridge.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* @param {string} options.name - Human-readable agent name (e.g. 'claims-processor')
|
|
20
|
+
* @param {string} options.type - Agent type: 'llm' | 'rpa' | 'workflow' | 'custom'
|
|
21
|
+
* @param {string} [options.model] - Model name (e.g. 'gpt-4o', 'claude-3-5-sonnet')
|
|
22
|
+
* @param {string[]} [options.scopes] - What this agent is allowed to do (e.g. ['read:claims', 'write:reports'])
|
|
23
|
+
* @param {object} [options.metadata] - Any additional metadata
|
|
24
|
+
* @returns {Promise<Agent>}
|
|
25
|
+
*/
|
|
26
|
+
async register(options = {}) {
|
|
27
|
+
if (!options.name) throw new Error('[NullBridge] agent.name is required');
|
|
28
|
+
if (!options.type) throw new Error('[NullBridge] agent.type is required');
|
|
29
|
+
|
|
30
|
+
const agentId = this._generateAgentId(options.name);
|
|
31
|
+
|
|
32
|
+
const payload = {
|
|
33
|
+
id: agentId,
|
|
34
|
+
name: options.name,
|
|
35
|
+
type: options.type,
|
|
36
|
+
model: options.model || null,
|
|
37
|
+
scopes: options.scopes || [],
|
|
38
|
+
metadata: options.metadata || {},
|
|
39
|
+
sdk_version: '1.0.0',
|
|
40
|
+
registered_at: Math.floor(Date.now() / 1000),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const { status, body } = await this._http.post(
|
|
45
|
+
this._config.apiUrl,
|
|
46
|
+
'/sdk/agents/register',
|
|
47
|
+
{ license_key: this._config.licenseKey, agent: payload }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const agent = status === 200 || status === 201
|
|
51
|
+
? { ...payload, ...body.agent, _registered: true }
|
|
52
|
+
: { ...payload, _registered: false, _local: true };
|
|
53
|
+
|
|
54
|
+
this._agents.set(agentId, agent);
|
|
55
|
+
console.info(`[NullBridge] Agent registered: ${options.name} (${agentId})`);
|
|
56
|
+
return new Agent(agent, this._config, this._http);
|
|
57
|
+
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// API unreachable — register locally and continue
|
|
60
|
+
const agent = { ...payload, _registered: false, _local: true };
|
|
61
|
+
this._agents.set(agentId, agent);
|
|
62
|
+
console.warn(`[NullBridge] Agent registered locally (API unreachable): ${options.name}`);
|
|
63
|
+
return new Agent(agent, this._config, this._http);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get all registered agents
|
|
69
|
+
*/
|
|
70
|
+
async list() {
|
|
71
|
+
try {
|
|
72
|
+
const { status, body } = await this._http.get(
|
|
73
|
+
this._config.apiUrl,
|
|
74
|
+
`/sdk/agents?license_key=${encodeURIComponent(this._config.licenseKey)}`
|
|
75
|
+
);
|
|
76
|
+
return status === 200 ? body.agents : Array.from(this._agents.values());
|
|
77
|
+
} catch {
|
|
78
|
+
return Array.from(this._agents.values());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a registered agent by ID
|
|
84
|
+
*/
|
|
85
|
+
get(agentId) {
|
|
86
|
+
const agent = this._agents.get(agentId);
|
|
87
|
+
if (!agent) return null;
|
|
88
|
+
return new Agent(agent, this._config, this._http);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_generateAgentId(name) {
|
|
92
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
93
|
+
const hash = crypto.createHash('sha256')
|
|
94
|
+
.update(this._config.licenseKey + name + Date.now())
|
|
95
|
+
.digest('hex')
|
|
96
|
+
.slice(0, 8);
|
|
97
|
+
return `agent-${slug}-${hash}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Agent — represents a single registered AI agent
|
|
103
|
+
*/
|
|
104
|
+
class Agent {
|
|
105
|
+
constructor(data, config, http) {
|
|
106
|
+
this._data = data;
|
|
107
|
+
this._config = config;
|
|
108
|
+
this._http = http;
|
|
109
|
+
|
|
110
|
+
// Expose agent properties
|
|
111
|
+
this.id = data.id;
|
|
112
|
+
this.name = data.name;
|
|
113
|
+
this.type = data.type;
|
|
114
|
+
this.model = data.model;
|
|
115
|
+
this.scopes = data.scopes || [];
|
|
116
|
+
this.metadata = data.metadata || {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Log an action this agent performed (creates an audit trail entry)
|
|
121
|
+
*
|
|
122
|
+
* @param {string} action - What the agent did (e.g. 'read_claim', 'send_email')
|
|
123
|
+
* @param {object} [details] - Additional context
|
|
124
|
+
*/
|
|
125
|
+
async logAction(action, details = {}) {
|
|
126
|
+
try {
|
|
127
|
+
await this._http.post(
|
|
128
|
+
this._config.apiUrl,
|
|
129
|
+
'/sdk/audit',
|
|
130
|
+
{
|
|
131
|
+
license_key: this._config.licenseKey,
|
|
132
|
+
agent_id: this.id,
|
|
133
|
+
agent_name: this.name,
|
|
134
|
+
action,
|
|
135
|
+
details,
|
|
136
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
} catch {
|
|
140
|
+
// Never throw from audit logging — don't break agent operation
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if this agent has a specific scope/permission
|
|
146
|
+
*
|
|
147
|
+
* @param {string} scope - e.g. 'write:claims'
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
hasScope(scope) {
|
|
151
|
+
return this.scopes.includes(scope) || this.scopes.includes('*');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update agent metadata or status
|
|
156
|
+
*/
|
|
157
|
+
async update(updates = {}) {
|
|
158
|
+
try {
|
|
159
|
+
await this._http.post(
|
|
160
|
+
this._config.apiUrl,
|
|
161
|
+
`/sdk/agents/${this.id}/update`,
|
|
162
|
+
{ license_key: this._config.licenseKey, ...updates }
|
|
163
|
+
);
|
|
164
|
+
Object.assign(this._data, updates);
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deregister this agent — revokes its identity and credentials
|
|
170
|
+
*/
|
|
171
|
+
async deregister() {
|
|
172
|
+
try {
|
|
173
|
+
await this._http.post(
|
|
174
|
+
this._config.apiUrl,
|
|
175
|
+
`/sdk/agents/${this.id}/deregister`,
|
|
176
|
+
{ license_key: this._config.licenseKey }
|
|
177
|
+
);
|
|
178
|
+
console.info(`[NullBridge] Agent deregistered: ${this.name}`);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.warn(`[NullBridge] Could not deregister agent remotely: ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
toJSON() {
|
|
185
|
+
return {
|
|
186
|
+
id: this.id, name: this.name, type: this.type,
|
|
187
|
+
model: this.model, scopes: this.scopes, metadata: this.metadata,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { AgentRegistry, Agent };
|
package/src/anomaly.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AnomalyDetector — behavioral baseline monitoring for AI agents
|
|
5
|
+
* Detects unusual patterns and alerts when agents deviate from normal behavior
|
|
6
|
+
*/
|
|
7
|
+
class AnomalyDetector {
|
|
8
|
+
constructor(config, http) {
|
|
9
|
+
this._config = config;
|
|
10
|
+
this._http = http;
|
|
11
|
+
this._baselines = new Map(); // agentId -> baseline metrics
|
|
12
|
+
this._handlers = []; // alert handlers
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Record a metric for an agent — used to build behavioral baseline
|
|
17
|
+
*
|
|
18
|
+
* @param {string} agentId
|
|
19
|
+
* @param {string} metric - e.g. 'api_calls_per_minute', 'tokens_used', 'unique_endpoints'
|
|
20
|
+
* @param {number} value
|
|
21
|
+
*/
|
|
22
|
+
record(agentId, metric, value) {
|
|
23
|
+
if (!this._baselines.has(agentId)) {
|
|
24
|
+
this._baselines.set(agentId, {});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const baseline = this._baselines.get(agentId);
|
|
28
|
+
if (!baseline[metric]) {
|
|
29
|
+
baseline[metric] = { samples: [], mean: 0, stddev: 0, count: 0 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const m = baseline[metric];
|
|
33
|
+
m.samples.push({ value, ts: Date.now() });
|
|
34
|
+
|
|
35
|
+
// Keep last 100 samples
|
|
36
|
+
if (m.samples.length > 100) m.samples.shift();
|
|
37
|
+
|
|
38
|
+
// Recalculate mean and stddev
|
|
39
|
+
const values = m.samples.map(s => s.value);
|
|
40
|
+
m.count = values.length;
|
|
41
|
+
m.mean = values.reduce((a, b) => a + b, 0) / m.count;
|
|
42
|
+
m.stddev = Math.sqrt(values.map(v => Math.pow(v - m.mean, 2)).reduce((a, b) => a + b, 0) / m.count);
|
|
43
|
+
|
|
44
|
+
// Check for anomaly (> 3 standard deviations from mean)
|
|
45
|
+
if (m.count >= 10 && m.stddev > 0) {
|
|
46
|
+
const zScore = Math.abs((value - m.mean) / m.stddev);
|
|
47
|
+
if (zScore > 3) {
|
|
48
|
+
this._triggerAlert(agentId, metric, value, m.mean, zScore);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a specific value is anomalous for an agent/metric
|
|
55
|
+
*
|
|
56
|
+
* @param {string} agentId
|
|
57
|
+
* @param {string} metric
|
|
58
|
+
* @param {number} value
|
|
59
|
+
* @returns {{ anomalous: boolean, zScore: number, mean: number }}
|
|
60
|
+
*/
|
|
61
|
+
check(agentId, metric, value) {
|
|
62
|
+
const baseline = this._baselines.get(agentId);
|
|
63
|
+
if (!baseline || !baseline[metric] || baseline[metric].count < 10) {
|
|
64
|
+
return { anomalous: false, zScore: 0, mean: value, reason: 'insufficient_data' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const m = baseline[metric];
|
|
68
|
+
const zScore = m.stddev > 0 ? Math.abs((value - m.mean) / m.stddev) : 0;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
anomalous: zScore > 3,
|
|
72
|
+
zScore: Math.round(zScore * 100) / 100,
|
|
73
|
+
mean: Math.round(m.mean * 100) / 100,
|
|
74
|
+
stddev: Math.round(m.stddev * 100) / 100,
|
|
75
|
+
samples: m.count,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Register an alert handler — called when anomaly is detected
|
|
81
|
+
*
|
|
82
|
+
* @param {function} handler - function(alert) where alert = { agentId, metric, value, mean, zScore }
|
|
83
|
+
*/
|
|
84
|
+
onAlert(handler) {
|
|
85
|
+
this._handlers.push(handler);
|
|
86
|
+
return this; // chainable
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get baseline metrics for an agent
|
|
91
|
+
*/
|
|
92
|
+
getBaseline(agentId) {
|
|
93
|
+
const baseline = this._baselines.get(agentId);
|
|
94
|
+
if (!baseline) return null;
|
|
95
|
+
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [metric, data] of Object.entries(baseline)) {
|
|
98
|
+
result[metric] = {
|
|
99
|
+
mean: Math.round(data.mean * 100) / 100,
|
|
100
|
+
stddev: Math.round(data.stddev * 100) / 100,
|
|
101
|
+
samples: data.count,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reset baseline for an agent (e.g. after a known behavior change)
|
|
109
|
+
*/
|
|
110
|
+
resetBaseline(agentId, metric = null) {
|
|
111
|
+
if (metric) {
|
|
112
|
+
const baseline = this._baselines.get(agentId);
|
|
113
|
+
if (baseline) delete baseline[metric];
|
|
114
|
+
} else {
|
|
115
|
+
this._baselines.delete(agentId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async _triggerAlert(agentId, metric, value, mean, zScore) {
|
|
120
|
+
const alert = {
|
|
121
|
+
agentId,
|
|
122
|
+
metric,
|
|
123
|
+
value,
|
|
124
|
+
mean: Math.round(mean * 100) / 100,
|
|
125
|
+
zScore: Math.round(zScore * 100) / 100,
|
|
126
|
+
severity: zScore > 6 ? 'critical' : zScore > 4 ? 'high' : 'medium',
|
|
127
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
console.warn(`[NullBridge] Anomaly detected — agent: ${agentId}, metric: ${metric}, value: ${value}, mean: ${mean.toFixed(2)}, z-score: ${zScore.toFixed(2)}`);
|
|
131
|
+
|
|
132
|
+
// Call registered handlers
|
|
133
|
+
for (const handler of this._handlers) {
|
|
134
|
+
try { handler(alert); } catch {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Send to NullBridge API
|
|
138
|
+
try {
|
|
139
|
+
await this._http.post(
|
|
140
|
+
this._config.apiUrl,
|
|
141
|
+
'/sdk/anomalies',
|
|
142
|
+
{ license_key: this._config.licenseKey, alert }
|
|
143
|
+
);
|
|
144
|
+
} catch {}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { AnomalyDetector };
|
package/src/audit.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AuditLogger — tamper-evident audit trail for all AI agent actions
|
|
5
|
+
*/
|
|
6
|
+
class AuditLogger {
|
|
7
|
+
constructor(config, http) {
|
|
8
|
+
this._config = config;
|
|
9
|
+
this._http = http;
|
|
10
|
+
this._queue = [];
|
|
11
|
+
this._flushing = false;
|
|
12
|
+
|
|
13
|
+
// Flush queue every 30 seconds
|
|
14
|
+
this._flushTimer = setInterval(() => this.flush(), 30 * 1000);
|
|
15
|
+
if (this._flushTimer.unref) this._flushTimer.unref();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Log an audit event
|
|
20
|
+
*
|
|
21
|
+
* @param {object} event
|
|
22
|
+
* @param {string} event.agentId - Agent that performed the action
|
|
23
|
+
* @param {string} event.agentName - Agent name
|
|
24
|
+
* @param {string} event.action - Action performed (e.g. 'read_claim', 'send_email', 'call_api')
|
|
25
|
+
* @param {string} [event.resource] - Resource acted on (e.g. 'claim:CLM-1234')
|
|
26
|
+
* @param {string} [event.outcome] - 'success' | 'failure' | 'blocked'
|
|
27
|
+
* @param {object} [event.details] - Additional context
|
|
28
|
+
* @param {string} [event.ip] - IP address if applicable
|
|
29
|
+
* @param {number} [event.duration] - Duration in ms
|
|
30
|
+
*/
|
|
31
|
+
log(event = {}) {
|
|
32
|
+
if (!event.agentId) throw new Error('[NullBridge] audit.log: agentId is required');
|
|
33
|
+
if (!event.action) throw new Error('[NullBridge] audit.log: action is required');
|
|
34
|
+
|
|
35
|
+
const entry = {
|
|
36
|
+
id: this._generateEventId(),
|
|
37
|
+
license_key: this._config.licenseKey,
|
|
38
|
+
agent_id: event.agentId,
|
|
39
|
+
agent_name: event.agentName || 'unknown',
|
|
40
|
+
action: event.action,
|
|
41
|
+
resource: event.resource || null,
|
|
42
|
+
outcome: event.outcome || 'success',
|
|
43
|
+
details: event.details || {},
|
|
44
|
+
ip: event.ip || null,
|
|
45
|
+
duration_ms: event.duration || null,
|
|
46
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this._queue.push(entry);
|
|
50
|
+
|
|
51
|
+
// Flush immediately for critical events
|
|
52
|
+
if (event.outcome === 'failure' || event.outcome === 'blocked') {
|
|
53
|
+
this.flush();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entry.id;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Log a policy violation — automatically sets outcome to 'blocked'
|
|
61
|
+
*/
|
|
62
|
+
logViolation(agentId, agentName, action, reason, details = {}) {
|
|
63
|
+
return this.log({
|
|
64
|
+
agentId, agentName, action,
|
|
65
|
+
outcome: 'blocked',
|
|
66
|
+
details: { reason, ...details },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Flush queued events to NullBridge API
|
|
72
|
+
*/
|
|
73
|
+
async flush() {
|
|
74
|
+
if (this._flushing || this._queue.length === 0) return;
|
|
75
|
+
this._flushing = true;
|
|
76
|
+
|
|
77
|
+
const batch = this._queue.splice(0, 100); // max 100 per flush
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await this._http.post(
|
|
81
|
+
this._config.apiUrl,
|
|
82
|
+
'/sdk/audit/batch',
|
|
83
|
+
{ events: batch }
|
|
84
|
+
);
|
|
85
|
+
} catch {
|
|
86
|
+
// Put events back in queue if flush fails
|
|
87
|
+
this._queue.unshift(...batch);
|
|
88
|
+
} finally {
|
|
89
|
+
this._flushing = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get queued (unflushed) events
|
|
95
|
+
*/
|
|
96
|
+
getQueue() {
|
|
97
|
+
return [...this._queue];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stop the flush timer
|
|
102
|
+
*/
|
|
103
|
+
stop() {
|
|
104
|
+
if (this._flushTimer) clearInterval(this._flushTimer);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_generateEventId() {
|
|
108
|
+
return 'evt-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { AuditLogger };
|