citadel0 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/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/index.cjs +398 -0
- package/dist/index.d.cts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +367 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moulick Bose
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Citadel 0
|
|
2
|
+
|
|
3
|
+
Programmatic security gateway and policy engine for AI agents built on Convex and TypeScript.
|
|
4
|
+
|
|
5
|
+
Citadel 0 inspects tool calls and actions before execution. It provides deterministic policy evaluation, heuristic threat scanning (prompt injections and secret leaks), and asynchronous human-in-the-loop (HITL) approval queues.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
- **Hybrid Evaluation Pipeline**: Evaluates deterministic rules first, followed by heuristic regex threat scanning.
|
|
12
|
+
- **Threat Scanner**: Detects prompt injection patterns, roleplay overrides, model token delimiters, and exposed API/private keys in parameters.
|
|
13
|
+
- **Human-in-the-Loop Reviews**: Pauses execution on `REVIEW` policy verdicts, logging a pending state to Convex and polling until resolved.
|
|
14
|
+
- **Tool Wrappers**: Adapters for wrapping function calls (`wrapTool` and `wrapToolWithReview`).
|
|
15
|
+
- **Audit Logging**: Logs every execution attempt, latency, risk score, and matched policies to Convex.
|
|
16
|
+
- **Dual Bundle**: Ships ESM and CommonJS builds with TypeScript declarations.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install citadel0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add citadel0
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Client Initialization
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { CitadelClient } from 'citadel0';
|
|
38
|
+
|
|
39
|
+
const citadel = new CitadelClient({
|
|
40
|
+
baseUrl: '[https://energetic-starfish-637.convex.site](https://energetic-starfish-637.convex.site)',
|
|
41
|
+
agentToken: process.env.CITADEL_AGENT_TOKEN!,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Action Check
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const verdict = await citadel.check('web.search', {
|
|
49
|
+
query: 'Convex database tutorial',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (verdict.verdict.decision === 'ALLOW') {
|
|
53
|
+
await runSearch();
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Direct Tool Guard
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { CitadelClient, wrapTool } from 'citadel0';
|
|
61
|
+
|
|
62
|
+
const citadel = new CitadelClient({
|
|
63
|
+
baseUrl: '[https://energetic-starfish-637.convex.site](https://energetic-starfish-637.convex.site)',
|
|
64
|
+
agentToken: process.env.CITADEL_AGENT_TOKEN!,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const executeCommand = wrapTool(citadel, {
|
|
68
|
+
name: 'system.bash',
|
|
69
|
+
execute: async (args: { command: string }) => {
|
|
70
|
+
return runCommand(args.command);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const output = await executeCommand.execute({ command: 'ls -la' });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Human-in-the-Loop (HITL) Review
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { CitadelClient, wrapToolWithReview } from 'citadel0';
|
|
81
|
+
|
|
82
|
+
const citadel = new CitadelClient({
|
|
83
|
+
baseUrl: '[https://energetic-starfish-637.convex.site](https://energetic-starfish-637.convex.site)',
|
|
84
|
+
agentToken: process.env.CITADEL_AGENT_TOKEN!,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const transferFunds = wrapToolWithReview(
|
|
88
|
+
citadel,
|
|
89
|
+
{
|
|
90
|
+
name: 'finance.transfer',
|
|
91
|
+
execute: async (args: { recipient: string; amount: number }) => {
|
|
92
|
+
return transfer(args.recipient, args.amount);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
timeoutMs: 60000,
|
|
97
|
+
pollIntervalMs: 2000,
|
|
98
|
+
onReviewPending: (auditLogId) => {
|
|
99
|
+
console.log(`Pending approval: ${auditLogId}`);
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
await transferFunds.execute({ recipient: 'alice@example.com', amount: 5000 });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### `CitadelClient`
|
|
112
|
+
|
|
113
|
+
- `citadel.check(action, params, options)`: Evaluates policies against the gateway and returns `SecurityVerdict`.
|
|
114
|
+
- `citadel.isAllowed(action, params, options)`: Returns boolean indicating if decision equals `ALLOW`.
|
|
115
|
+
- `citadel.guard(action, params, callback, options)`: Runs callback if allowed; throws error if blocked or requiring review.
|
|
116
|
+
- `citadel.guardWithReview(action, params, callback, options)`: Runs callback if allowed. If flagged for review, polls until approved or timed out.
|
|
117
|
+
- `citadel.getReviewStatus(auditLogId)`: Checks resolution status of an audit log.
|
|
118
|
+
|
|
119
|
+
### Utilities
|
|
120
|
+
|
|
121
|
+
- `wrapTool(client, toolDefinition)`: Guards an execution function with deterministic and semantic policy checks.
|
|
122
|
+
- `wrapToolWithReview(client, toolDefinition, options)`: Guards an execution function with review polling.
|
|
123
|
+
- `scanPayloadSemantics(params)`: Runs offline heuristic threat scanning against input parameters.
|
|
124
|
+
- `evaluatePolicies(payload, policies)`: Runs local deterministic and semantic engine directly.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CitadelClient: () => CitadelClient,
|
|
24
|
+
evaluatePolicies: () => evaluatePolicies,
|
|
25
|
+
scanPayloadSemantics: () => scanPayloadSemantics,
|
|
26
|
+
wrapTool: () => wrapTool,
|
|
27
|
+
wrapToolWithReview: () => wrapToolWithReview
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/sdk/client.ts
|
|
32
|
+
var CitadelClient = class {
|
|
33
|
+
baseUrl;
|
|
34
|
+
agentToken;
|
|
35
|
+
environment;
|
|
36
|
+
defaultSessionId;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
39
|
+
this.agentToken = config.agentToken;
|
|
40
|
+
this.environment = config.environment;
|
|
41
|
+
this.defaultSessionId = config.defaultSessionId;
|
|
42
|
+
}
|
|
43
|
+
async check(action, params = {}, options = {}) {
|
|
44
|
+
const url = `${this.baseUrl}/api/v1/check`;
|
|
45
|
+
const sessionId = options.sessionId ?? this.defaultSessionId ?? `sess_${Math.random().toString(36).slice(2, 11)}`;
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
Authorization: `Bearer ${this.agentToken}`
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
action,
|
|
54
|
+
params,
|
|
55
|
+
sessionId,
|
|
56
|
+
environment: options.environment ?? this.environment,
|
|
57
|
+
metadata: options.metadata
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
62
|
+
const errorMessage = typeof errorBody.error === "string" ? errorBody.error : `Citadel gateway error: ${response.status} ${response.statusText}`;
|
|
63
|
+
throw new Error(errorMessage);
|
|
64
|
+
}
|
|
65
|
+
return await response.json();
|
|
66
|
+
}
|
|
67
|
+
async isAllowed(action, params = {}, options = {}) {
|
|
68
|
+
const result = await this.check(action, params, options);
|
|
69
|
+
return result.verdict.decision === "ALLOW";
|
|
70
|
+
}
|
|
71
|
+
async guard(action, params, callback, options = {}) {
|
|
72
|
+
const result = await this.check(action, params, options);
|
|
73
|
+
if (result.verdict.decision !== "ALLOW") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Action '${action}' blocked by Citadel: ${result.verdict.reason}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return await callback();
|
|
79
|
+
}
|
|
80
|
+
async getReviewStatus(auditLogId) {
|
|
81
|
+
const url = `${this.baseUrl}/api/v1/review/status?auditLogId=${encodeURIComponent(
|
|
82
|
+
auditLogId
|
|
83
|
+
)}`;
|
|
84
|
+
const response = await fetch(url);
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
87
|
+
const errorMessage = typeof errorBody.error === "string" ? errorBody.error : `Review status error: ${response.status}`;
|
|
88
|
+
throw new Error(errorMessage);
|
|
89
|
+
}
|
|
90
|
+
return await response.json();
|
|
91
|
+
}
|
|
92
|
+
async pollReview(auditLogId, options = {}) {
|
|
93
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
94
|
+
const intervalMs = options.intervalMs ?? 2e3;
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
while (Date.now() < deadline) {
|
|
97
|
+
const status = await this.getReviewStatus(auditLogId);
|
|
98
|
+
if (status.decision !== "REVIEW") {
|
|
99
|
+
return status;
|
|
100
|
+
}
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
102
|
+
}
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Review timed out after ${timeoutMs}ms for auditLogId: ${auditLogId}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
async guardWithReview(action, params, callback, options = {}) {
|
|
108
|
+
const result = await this.check(action, params, options);
|
|
109
|
+
if (result.verdict.decision === "ALLOW") {
|
|
110
|
+
return await callback();
|
|
111
|
+
}
|
|
112
|
+
if (result.verdict.decision === "BLOCK") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Action '${action}' blocked by Citadel: ${result.verdict.reason}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (options.onReviewPending) {
|
|
118
|
+
options.onReviewPending(result.auditLogId);
|
|
119
|
+
}
|
|
120
|
+
const reviewed = await this.pollReview(result.auditLogId, {
|
|
121
|
+
timeoutMs: options.timeoutMs,
|
|
122
|
+
intervalMs: options.pollIntervalMs
|
|
123
|
+
});
|
|
124
|
+
if (reviewed.decision === "ALLOW") {
|
|
125
|
+
return await callback();
|
|
126
|
+
}
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Action '${action}' rejected during human review: ${reviewed.reviewComment ?? "Rejected"}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/sdk/tools.ts
|
|
134
|
+
function wrapTool(client, tool) {
|
|
135
|
+
return {
|
|
136
|
+
...tool,
|
|
137
|
+
execute: async (args) => {
|
|
138
|
+
return await client.guard(
|
|
139
|
+
tool.name,
|
|
140
|
+
args,
|
|
141
|
+
() => tool.execute(args),
|
|
142
|
+
tool.options
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function wrapToolWithReview(client, tool, reviewOptions) {
|
|
148
|
+
return {
|
|
149
|
+
...tool,
|
|
150
|
+
execute: async (args) => {
|
|
151
|
+
return await client.guardWithReview(
|
|
152
|
+
tool.name,
|
|
153
|
+
args,
|
|
154
|
+
() => tool.execute(args),
|
|
155
|
+
{
|
|
156
|
+
...tool.options,
|
|
157
|
+
...reviewOptions
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/engine/semantic.ts
|
|
165
|
+
var INJECTION_PATTERNS = [
|
|
166
|
+
{
|
|
167
|
+
regex: /(?:ignore|disregard|forget|bypass|override)\s+(?:all\s+)?(?:previous|prior|existing|above)\s+(?:instructions|rules|guidelines|directions|constraints)/i,
|
|
168
|
+
description: "Direct prompt instruction override",
|
|
169
|
+
score: 95
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
regex: /(?:you\s+are\s+now|act\s+as|roleplay\s+as)\s+(?:an?\s+)?(?:unfiltered|unrestricted|jailbroken|evil|dan|developer\s+mode)/i,
|
|
173
|
+
description: "Adversarial persona / DAN jailbreak attempt",
|
|
174
|
+
score: 90
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
regex: /(?:reveal|output|print|display|leak)\s+(?:the\s+)?(?:system\s+prompt|initial\s+prompt|core\s+instructions|developer\s+instructions)/i,
|
|
178
|
+
description: "System prompt extraction attempt",
|
|
179
|
+
score: 85
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
regex: /(?:\[INST\]|\[\/INST\]|<<SYS>>|<\/SYS>|<\|im_start\|>|<\|im_end\|>)/i,
|
|
183
|
+
description: "Model token delimiter injection",
|
|
184
|
+
score: 90
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
var CREDENTIAL_PATTERNS = [
|
|
188
|
+
{
|
|
189
|
+
regex: /(?:sk-[a-zA-Z0-9-_]{20,}|ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36})/i,
|
|
190
|
+
description: "Exposed API token (OpenAI / GitHub)",
|
|
191
|
+
score: 95
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
195
|
+
description: "AWS Access Key ID",
|
|
196
|
+
score: 90
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
|
|
200
|
+
description: "Exposed Private Key block",
|
|
201
|
+
score: 99
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
function extractStrings(obj) {
|
|
205
|
+
const strings = [];
|
|
206
|
+
function recurse(value) {
|
|
207
|
+
if (typeof value === "string") {
|
|
208
|
+
strings.push(value);
|
|
209
|
+
} else if (Array.isArray(value)) {
|
|
210
|
+
for (const item of value) recurse(item);
|
|
211
|
+
} else if (value !== null && typeof value === "object") {
|
|
212
|
+
for (const val of Object.values(value)) {
|
|
213
|
+
recurse(val);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
recurse(obj);
|
|
218
|
+
return strings;
|
|
219
|
+
}
|
|
220
|
+
function scanPayloadSemantics(params) {
|
|
221
|
+
const stringsToScan = extractStrings(params);
|
|
222
|
+
const threats = [];
|
|
223
|
+
for (const text of stringsToScan) {
|
|
224
|
+
for (const rule of INJECTION_PATTERNS) {
|
|
225
|
+
if (rule.regex.test(text)) {
|
|
226
|
+
threats.push({
|
|
227
|
+
type: "PROMPT_INJECTION",
|
|
228
|
+
confidence: rule.score / 100,
|
|
229
|
+
reason: rule.description,
|
|
230
|
+
matchedPattern: rule.regex.source
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const rule of CREDENTIAL_PATTERNS) {
|
|
235
|
+
if (rule.regex.test(text)) {
|
|
236
|
+
threats.push({
|
|
237
|
+
type: "CREDENTIAL_LEAK",
|
|
238
|
+
confidence: rule.score / 100,
|
|
239
|
+
reason: rule.description,
|
|
240
|
+
matchedPattern: rule.regex.source
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const maxScore = threats.length > 0 ? Math.max(...threats.map((t) => t.confidence * 100)) : 0;
|
|
246
|
+
return {
|
|
247
|
+
flagged: threats.length > 0,
|
|
248
|
+
threats,
|
|
249
|
+
maxRiskScore: Math.round(maxScore)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/engine/match.ts
|
|
254
|
+
function extractField(obj, path) {
|
|
255
|
+
const parts = path.split(".");
|
|
256
|
+
let current = obj;
|
|
257
|
+
for (const part of parts) {
|
|
258
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
current = current[part];
|
|
262
|
+
}
|
|
263
|
+
return current;
|
|
264
|
+
}
|
|
265
|
+
function evaluateCondition(actual, operator, target) {
|
|
266
|
+
if (actual === void 0 || actual === null) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
switch (operator) {
|
|
270
|
+
case "EQUALS":
|
|
271
|
+
return actual === target;
|
|
272
|
+
case "NOT_EQUALS":
|
|
273
|
+
return actual !== target;
|
|
274
|
+
case "GREATER_THAN":
|
|
275
|
+
return typeof actual === "number" && typeof target === "number" && actual > target;
|
|
276
|
+
case "LESS_THAN":
|
|
277
|
+
return typeof actual === "number" && typeof target === "number" && actual < target;
|
|
278
|
+
case "IN":
|
|
279
|
+
return Array.isArray(target) && target.includes(actual);
|
|
280
|
+
case "NOT_IN":
|
|
281
|
+
return Array.isArray(target) && !target.includes(actual);
|
|
282
|
+
case "CONTAINS":
|
|
283
|
+
if (typeof actual === "string" && typeof target === "string") {
|
|
284
|
+
return actual.includes(target);
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(actual)) {
|
|
287
|
+
return actual.includes(target);
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
case "REGEX_MATCH":
|
|
291
|
+
if (typeof actual !== "string" || typeof target !== "string") {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
return new RegExp(target).test(actual);
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
default:
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function matchesActionPattern(pattern, action) {
|
|
304
|
+
if (pattern === "*" || pattern === action) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (pattern.endsWith("*")) {
|
|
308
|
+
const prefix = pattern.slice(0, -1);
|
|
309
|
+
return action.startsWith(prefix);
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
function evaluateRule(payload, rule) {
|
|
314
|
+
const targetObject = {
|
|
315
|
+
action: payload.action,
|
|
316
|
+
params: payload.params,
|
|
317
|
+
context: payload.context,
|
|
318
|
+
timestamp: payload.timestamp,
|
|
319
|
+
metadata: payload.metadata ?? {}
|
|
320
|
+
};
|
|
321
|
+
const actualValue = extractField(targetObject, rule.field);
|
|
322
|
+
return evaluateCondition(actualValue, rule.operator, rule.value);
|
|
323
|
+
}
|
|
324
|
+
function calculateRisk(decision, matchedCount, semanticScore = 0) {
|
|
325
|
+
if (decision === "BLOCK") {
|
|
326
|
+
return { riskScore: Math.max(95, semanticScore), riskLevel: "CRITICAL" };
|
|
327
|
+
}
|
|
328
|
+
if (decision === "REVIEW") {
|
|
329
|
+
return { riskScore: Math.max(65, semanticScore), riskLevel: "HIGH" };
|
|
330
|
+
}
|
|
331
|
+
if (matchedCount > 0) {
|
|
332
|
+
return { riskScore: 25, riskLevel: "MEDIUM" };
|
|
333
|
+
}
|
|
334
|
+
return { riskScore: 5, riskLevel: "LOW" };
|
|
335
|
+
}
|
|
336
|
+
function evaluatePolicies(payload, policies) {
|
|
337
|
+
const startTime = Date.now();
|
|
338
|
+
const traces = [];
|
|
339
|
+
const activePolicies = policies.filter((p) => p.isActive && p.tenantId === payload.context.tenantId).sort((a, b) => b.priority - a.priority);
|
|
340
|
+
let finalDecision = "ALLOW";
|
|
341
|
+
let primaryReason = "No blocking or review policies triggered";
|
|
342
|
+
let pipeline = "DETERMINISTIC";
|
|
343
|
+
for (const policy of activePolicies) {
|
|
344
|
+
if (!matchesActionPattern(policy.actionPattern, payload.action)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const rulesMatched = policy.rules.length > 0 && policy.rules.every((rule) => evaluateRule(payload, rule));
|
|
348
|
+
if (rulesMatched) {
|
|
349
|
+
traces.push({
|
|
350
|
+
policyId: policy.id,
|
|
351
|
+
policyName: policy.name,
|
|
352
|
+
matched: true,
|
|
353
|
+
effect: policy.effect
|
|
354
|
+
});
|
|
355
|
+
if (policy.effect === "BLOCK") {
|
|
356
|
+
finalDecision = "BLOCK";
|
|
357
|
+
primaryReason = `Blocked by policy: ${policy.name}`;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (policy.effect === "REVIEW") {
|
|
361
|
+
finalDecision = "REVIEW";
|
|
362
|
+
primaryReason = `Review required by policy: ${policy.name}`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
let semanticScore = 0;
|
|
367
|
+
if (finalDecision !== "BLOCK") {
|
|
368
|
+
const semanticResult = scanPayloadSemantics(payload.params);
|
|
369
|
+
if (semanticResult.flagged) {
|
|
370
|
+
pipeline = traces.length > 0 ? "HYBRID" : "AI_GUARD";
|
|
371
|
+
semanticScore = semanticResult.maxRiskScore;
|
|
372
|
+
finalDecision = "BLOCK";
|
|
373
|
+
const threatReasons = semanticResult.threats.map((t) => t.reason).join(", ");
|
|
374
|
+
primaryReason = `AI Guard blocked threat: ${threatReasons}`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const { riskScore, riskLevel } = calculateRisk(finalDecision, traces.length, semanticScore);
|
|
378
|
+
const latencyMs = Date.now() - startTime;
|
|
379
|
+
return {
|
|
380
|
+
verdictId: `vrd_${Math.random().toString(36).slice(2, 11)}`,
|
|
381
|
+
decision: finalDecision,
|
|
382
|
+
reason: primaryReason,
|
|
383
|
+
riskScore,
|
|
384
|
+
riskLevel,
|
|
385
|
+
evaluationPipeline: pipeline,
|
|
386
|
+
matchedPolicies: traces,
|
|
387
|
+
executedAt: startTime,
|
|
388
|
+
latencyMs
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
392
|
+
0 && (module.exports = {
|
|
393
|
+
CitadelClient,
|
|
394
|
+
evaluatePolicies,
|
|
395
|
+
scanPayloadSemantics,
|
|
396
|
+
wrapTool,
|
|
397
|
+
wrapToolWithReview
|
|
398
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
type Decision = 'ALLOW' | 'BLOCK' | 'REVIEW';
|
|
2
|
+
type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
3
|
+
type PolicyEffect = 'ALLOW' | 'BLOCK' | 'REVIEW';
|
|
4
|
+
interface SecurityContext {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
agentId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
environment: 'production' | 'staging' | 'development';
|
|
9
|
+
clientIp?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ActionPayload {
|
|
12
|
+
context: SecurityContext;
|
|
13
|
+
action: string;
|
|
14
|
+
params: Record<string, unknown>;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
type PolicyConditionOperator = 'EQUALS' | 'NOT_EQUALS' | 'GREATER_THAN' | 'LESS_THAN' | 'IN' | 'NOT_IN' | 'CONTAINS' | 'REGEX_MATCH';
|
|
19
|
+
interface PolicyRule {
|
|
20
|
+
field: string;
|
|
21
|
+
operator: PolicyConditionOperator;
|
|
22
|
+
value: unknown;
|
|
23
|
+
}
|
|
24
|
+
interface SecurityPolicy {
|
|
25
|
+
id: string;
|
|
26
|
+
tenantId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
actionPattern: string;
|
|
30
|
+
rules: PolicyRule[];
|
|
31
|
+
effect: PolicyEffect;
|
|
32
|
+
priority: number;
|
|
33
|
+
isActive: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface PolicyEvaluationTrace {
|
|
36
|
+
policyId: string;
|
|
37
|
+
policyName: string;
|
|
38
|
+
matched: boolean;
|
|
39
|
+
effect: PolicyEffect;
|
|
40
|
+
}
|
|
41
|
+
interface SecurityVerdict {
|
|
42
|
+
verdictId: string;
|
|
43
|
+
decision: Decision;
|
|
44
|
+
reason: string;
|
|
45
|
+
riskScore: number;
|
|
46
|
+
riskLevel: RiskLevel;
|
|
47
|
+
evaluationPipeline: 'DETERMINISTIC' | 'AI_GUARD' | 'HYBRID';
|
|
48
|
+
matchedPolicies: PolicyEvaluationTrace[];
|
|
49
|
+
executedAt: number;
|
|
50
|
+
latencyMs: number;
|
|
51
|
+
}
|
|
52
|
+
interface AuditLogEntry {
|
|
53
|
+
id: string;
|
|
54
|
+
tenantId: string;
|
|
55
|
+
agentId: string;
|
|
56
|
+
sessionId: string;
|
|
57
|
+
action: string;
|
|
58
|
+
params: Record<string, unknown>;
|
|
59
|
+
verdict: SecurityVerdict;
|
|
60
|
+
reviewedBy?: string;
|
|
61
|
+
reviewComment?: string;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface CitadelClientConfig {
|
|
66
|
+
baseUrl: string;
|
|
67
|
+
agentToken: string;
|
|
68
|
+
environment?: 'production' | 'staging' | 'development';
|
|
69
|
+
defaultSessionId?: string;
|
|
70
|
+
}
|
|
71
|
+
interface CheckOptions {
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
environment?: 'production' | 'staging' | 'development';
|
|
74
|
+
metadata?: Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
interface CheckResult {
|
|
77
|
+
verdict: SecurityVerdict;
|
|
78
|
+
auditLogId: string;
|
|
79
|
+
}
|
|
80
|
+
interface ReviewStatus {
|
|
81
|
+
auditLogId: string;
|
|
82
|
+
decision: 'ALLOW' | 'BLOCK' | 'REVIEW';
|
|
83
|
+
reviewedBy?: string;
|
|
84
|
+
reviewComment?: string;
|
|
85
|
+
action: string;
|
|
86
|
+
params: unknown;
|
|
87
|
+
riskScore: number;
|
|
88
|
+
riskLevel: string;
|
|
89
|
+
createdAt: number;
|
|
90
|
+
}
|
|
91
|
+
interface GuardWithReviewOptions extends CheckOptions {
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
pollIntervalMs?: number;
|
|
94
|
+
onReviewPending?: (auditLogId: string) => void;
|
|
95
|
+
}
|
|
96
|
+
declare class CitadelClient {
|
|
97
|
+
private readonly baseUrl;
|
|
98
|
+
private readonly agentToken;
|
|
99
|
+
private readonly environment?;
|
|
100
|
+
private readonly defaultSessionId?;
|
|
101
|
+
constructor(config: CitadelClientConfig);
|
|
102
|
+
check(action: string, params?: Record<string, unknown>, options?: CheckOptions): Promise<CheckResult>;
|
|
103
|
+
isAllowed(action: string, params?: Record<string, unknown>, options?: CheckOptions): Promise<boolean>;
|
|
104
|
+
guard<T>(action: string, params: Record<string, unknown>, callback: () => Promise<T> | T, options?: CheckOptions): Promise<T>;
|
|
105
|
+
getReviewStatus(auditLogId: string): Promise<ReviewStatus>;
|
|
106
|
+
pollReview(auditLogId: string, options?: {
|
|
107
|
+
timeoutMs?: number;
|
|
108
|
+
intervalMs?: number;
|
|
109
|
+
}): Promise<ReviewStatus>;
|
|
110
|
+
guardWithReview<T>(action: string, params: Record<string, unknown>, callback: () => Promise<T> | T, options?: GuardWithReviewOptions): Promise<T>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface GuardedToolDefinition<TArgs = any, TResult = any> {
|
|
114
|
+
name: string;
|
|
115
|
+
description?: string;
|
|
116
|
+
execute: (args: TArgs) => Promise<TResult> | TResult;
|
|
117
|
+
options?: CheckOptions;
|
|
118
|
+
}
|
|
119
|
+
declare function wrapTool<TArgs extends Record<string, unknown>, TResult>(client: CitadelClient, tool: GuardedToolDefinition<TArgs, TResult>): {
|
|
120
|
+
execute: (args: TArgs) => Promise<TResult>;
|
|
121
|
+
name: string;
|
|
122
|
+
description?: string;
|
|
123
|
+
options?: CheckOptions;
|
|
124
|
+
};
|
|
125
|
+
declare function wrapToolWithReview<TArgs extends Record<string, unknown>, TResult>(client: CitadelClient, tool: GuardedToolDefinition<TArgs, TResult>, reviewOptions?: {
|
|
126
|
+
timeoutMs?: number;
|
|
127
|
+
pollIntervalMs?: number;
|
|
128
|
+
onReviewPending?: (auditLogId: string) => void;
|
|
129
|
+
}): {
|
|
130
|
+
execute: (args: TArgs) => Promise<TResult>;
|
|
131
|
+
name: string;
|
|
132
|
+
description?: string;
|
|
133
|
+
options?: CheckOptions;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
interface ThreatDetection {
|
|
137
|
+
type: 'PROMPT_INJECTION' | 'JAILBREAK_ATTEMPT' | 'CREDENTIAL_LEAK';
|
|
138
|
+
confidence: number;
|
|
139
|
+
reason: string;
|
|
140
|
+
matchedPattern: string;
|
|
141
|
+
}
|
|
142
|
+
interface SemanticScanResult {
|
|
143
|
+
flagged: boolean;
|
|
144
|
+
threats: ThreatDetection[];
|
|
145
|
+
maxRiskScore: number;
|
|
146
|
+
}
|
|
147
|
+
declare function scanPayloadSemantics(params: Record<string, unknown>): SemanticScanResult;
|
|
148
|
+
|
|
149
|
+
declare function evaluatePolicies(payload: ActionPayload, policies: SecurityPolicy[]): SecurityVerdict;
|
|
150
|
+
|
|
151
|
+
export { type ActionPayload, type AuditLogEntry, type CheckOptions, type CheckResult, CitadelClient, type CitadelClientConfig, type Decision, type GuardWithReviewOptions, type GuardedToolDefinition, type PolicyConditionOperator, type PolicyEffect, type PolicyEvaluationTrace, type PolicyRule, type ReviewStatus, type RiskLevel, type SecurityContext, type SecurityPolicy, type SecurityVerdict, type SemanticScanResult, type ThreatDetection, evaluatePolicies, scanPayloadSemantics, wrapTool, wrapToolWithReview };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
type Decision = 'ALLOW' | 'BLOCK' | 'REVIEW';
|
|
2
|
+
type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
3
|
+
type PolicyEffect = 'ALLOW' | 'BLOCK' | 'REVIEW';
|
|
4
|
+
interface SecurityContext {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
agentId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
environment: 'production' | 'staging' | 'development';
|
|
9
|
+
clientIp?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ActionPayload {
|
|
12
|
+
context: SecurityContext;
|
|
13
|
+
action: string;
|
|
14
|
+
params: Record<string, unknown>;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
type PolicyConditionOperator = 'EQUALS' | 'NOT_EQUALS' | 'GREATER_THAN' | 'LESS_THAN' | 'IN' | 'NOT_IN' | 'CONTAINS' | 'REGEX_MATCH';
|
|
19
|
+
interface PolicyRule {
|
|
20
|
+
field: string;
|
|
21
|
+
operator: PolicyConditionOperator;
|
|
22
|
+
value: unknown;
|
|
23
|
+
}
|
|
24
|
+
interface SecurityPolicy {
|
|
25
|
+
id: string;
|
|
26
|
+
tenantId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
actionPattern: string;
|
|
30
|
+
rules: PolicyRule[];
|
|
31
|
+
effect: PolicyEffect;
|
|
32
|
+
priority: number;
|
|
33
|
+
isActive: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface PolicyEvaluationTrace {
|
|
36
|
+
policyId: string;
|
|
37
|
+
policyName: string;
|
|
38
|
+
matched: boolean;
|
|
39
|
+
effect: PolicyEffect;
|
|
40
|
+
}
|
|
41
|
+
interface SecurityVerdict {
|
|
42
|
+
verdictId: string;
|
|
43
|
+
decision: Decision;
|
|
44
|
+
reason: string;
|
|
45
|
+
riskScore: number;
|
|
46
|
+
riskLevel: RiskLevel;
|
|
47
|
+
evaluationPipeline: 'DETERMINISTIC' | 'AI_GUARD' | 'HYBRID';
|
|
48
|
+
matchedPolicies: PolicyEvaluationTrace[];
|
|
49
|
+
executedAt: number;
|
|
50
|
+
latencyMs: number;
|
|
51
|
+
}
|
|
52
|
+
interface AuditLogEntry {
|
|
53
|
+
id: string;
|
|
54
|
+
tenantId: string;
|
|
55
|
+
agentId: string;
|
|
56
|
+
sessionId: string;
|
|
57
|
+
action: string;
|
|
58
|
+
params: Record<string, unknown>;
|
|
59
|
+
verdict: SecurityVerdict;
|
|
60
|
+
reviewedBy?: string;
|
|
61
|
+
reviewComment?: string;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface CitadelClientConfig {
|
|
66
|
+
baseUrl: string;
|
|
67
|
+
agentToken: string;
|
|
68
|
+
environment?: 'production' | 'staging' | 'development';
|
|
69
|
+
defaultSessionId?: string;
|
|
70
|
+
}
|
|
71
|
+
interface CheckOptions {
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
environment?: 'production' | 'staging' | 'development';
|
|
74
|
+
metadata?: Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
interface CheckResult {
|
|
77
|
+
verdict: SecurityVerdict;
|
|
78
|
+
auditLogId: string;
|
|
79
|
+
}
|
|
80
|
+
interface ReviewStatus {
|
|
81
|
+
auditLogId: string;
|
|
82
|
+
decision: 'ALLOW' | 'BLOCK' | 'REVIEW';
|
|
83
|
+
reviewedBy?: string;
|
|
84
|
+
reviewComment?: string;
|
|
85
|
+
action: string;
|
|
86
|
+
params: unknown;
|
|
87
|
+
riskScore: number;
|
|
88
|
+
riskLevel: string;
|
|
89
|
+
createdAt: number;
|
|
90
|
+
}
|
|
91
|
+
interface GuardWithReviewOptions extends CheckOptions {
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
pollIntervalMs?: number;
|
|
94
|
+
onReviewPending?: (auditLogId: string) => void;
|
|
95
|
+
}
|
|
96
|
+
declare class CitadelClient {
|
|
97
|
+
private readonly baseUrl;
|
|
98
|
+
private readonly agentToken;
|
|
99
|
+
private readonly environment?;
|
|
100
|
+
private readonly defaultSessionId?;
|
|
101
|
+
constructor(config: CitadelClientConfig);
|
|
102
|
+
check(action: string, params?: Record<string, unknown>, options?: CheckOptions): Promise<CheckResult>;
|
|
103
|
+
isAllowed(action: string, params?: Record<string, unknown>, options?: CheckOptions): Promise<boolean>;
|
|
104
|
+
guard<T>(action: string, params: Record<string, unknown>, callback: () => Promise<T> | T, options?: CheckOptions): Promise<T>;
|
|
105
|
+
getReviewStatus(auditLogId: string): Promise<ReviewStatus>;
|
|
106
|
+
pollReview(auditLogId: string, options?: {
|
|
107
|
+
timeoutMs?: number;
|
|
108
|
+
intervalMs?: number;
|
|
109
|
+
}): Promise<ReviewStatus>;
|
|
110
|
+
guardWithReview<T>(action: string, params: Record<string, unknown>, callback: () => Promise<T> | T, options?: GuardWithReviewOptions): Promise<T>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface GuardedToolDefinition<TArgs = any, TResult = any> {
|
|
114
|
+
name: string;
|
|
115
|
+
description?: string;
|
|
116
|
+
execute: (args: TArgs) => Promise<TResult> | TResult;
|
|
117
|
+
options?: CheckOptions;
|
|
118
|
+
}
|
|
119
|
+
declare function wrapTool<TArgs extends Record<string, unknown>, TResult>(client: CitadelClient, tool: GuardedToolDefinition<TArgs, TResult>): {
|
|
120
|
+
execute: (args: TArgs) => Promise<TResult>;
|
|
121
|
+
name: string;
|
|
122
|
+
description?: string;
|
|
123
|
+
options?: CheckOptions;
|
|
124
|
+
};
|
|
125
|
+
declare function wrapToolWithReview<TArgs extends Record<string, unknown>, TResult>(client: CitadelClient, tool: GuardedToolDefinition<TArgs, TResult>, reviewOptions?: {
|
|
126
|
+
timeoutMs?: number;
|
|
127
|
+
pollIntervalMs?: number;
|
|
128
|
+
onReviewPending?: (auditLogId: string) => void;
|
|
129
|
+
}): {
|
|
130
|
+
execute: (args: TArgs) => Promise<TResult>;
|
|
131
|
+
name: string;
|
|
132
|
+
description?: string;
|
|
133
|
+
options?: CheckOptions;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
interface ThreatDetection {
|
|
137
|
+
type: 'PROMPT_INJECTION' | 'JAILBREAK_ATTEMPT' | 'CREDENTIAL_LEAK';
|
|
138
|
+
confidence: number;
|
|
139
|
+
reason: string;
|
|
140
|
+
matchedPattern: string;
|
|
141
|
+
}
|
|
142
|
+
interface SemanticScanResult {
|
|
143
|
+
flagged: boolean;
|
|
144
|
+
threats: ThreatDetection[];
|
|
145
|
+
maxRiskScore: number;
|
|
146
|
+
}
|
|
147
|
+
declare function scanPayloadSemantics(params: Record<string, unknown>): SemanticScanResult;
|
|
148
|
+
|
|
149
|
+
declare function evaluatePolicies(payload: ActionPayload, policies: SecurityPolicy[]): SecurityVerdict;
|
|
150
|
+
|
|
151
|
+
export { type ActionPayload, type AuditLogEntry, type CheckOptions, type CheckResult, CitadelClient, type CitadelClientConfig, type Decision, type GuardWithReviewOptions, type GuardedToolDefinition, type PolicyConditionOperator, type PolicyEffect, type PolicyEvaluationTrace, type PolicyRule, type ReviewStatus, type RiskLevel, type SecurityContext, type SecurityPolicy, type SecurityVerdict, type SemanticScanResult, type ThreatDetection, evaluatePolicies, scanPayloadSemantics, wrapTool, wrapToolWithReview };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// src/sdk/client.ts
|
|
2
|
+
var CitadelClient = class {
|
|
3
|
+
baseUrl;
|
|
4
|
+
agentToken;
|
|
5
|
+
environment;
|
|
6
|
+
defaultSessionId;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
9
|
+
this.agentToken = config.agentToken;
|
|
10
|
+
this.environment = config.environment;
|
|
11
|
+
this.defaultSessionId = config.defaultSessionId;
|
|
12
|
+
}
|
|
13
|
+
async check(action, params = {}, options = {}) {
|
|
14
|
+
const url = `${this.baseUrl}/api/v1/check`;
|
|
15
|
+
const sessionId = options.sessionId ?? this.defaultSessionId ?? `sess_${Math.random().toString(36).slice(2, 11)}`;
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Authorization: `Bearer ${this.agentToken}`
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
action,
|
|
24
|
+
params,
|
|
25
|
+
sessionId,
|
|
26
|
+
environment: options.environment ?? this.environment,
|
|
27
|
+
metadata: options.metadata
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
32
|
+
const errorMessage = typeof errorBody.error === "string" ? errorBody.error : `Citadel gateway error: ${response.status} ${response.statusText}`;
|
|
33
|
+
throw new Error(errorMessage);
|
|
34
|
+
}
|
|
35
|
+
return await response.json();
|
|
36
|
+
}
|
|
37
|
+
async isAllowed(action, params = {}, options = {}) {
|
|
38
|
+
const result = await this.check(action, params, options);
|
|
39
|
+
return result.verdict.decision === "ALLOW";
|
|
40
|
+
}
|
|
41
|
+
async guard(action, params, callback, options = {}) {
|
|
42
|
+
const result = await this.check(action, params, options);
|
|
43
|
+
if (result.verdict.decision !== "ALLOW") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Action '${action}' blocked by Citadel: ${result.verdict.reason}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return await callback();
|
|
49
|
+
}
|
|
50
|
+
async getReviewStatus(auditLogId) {
|
|
51
|
+
const url = `${this.baseUrl}/api/v1/review/status?auditLogId=${encodeURIComponent(
|
|
52
|
+
auditLogId
|
|
53
|
+
)}`;
|
|
54
|
+
const response = await fetch(url);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
57
|
+
const errorMessage = typeof errorBody.error === "string" ? errorBody.error : `Review status error: ${response.status}`;
|
|
58
|
+
throw new Error(errorMessage);
|
|
59
|
+
}
|
|
60
|
+
return await response.json();
|
|
61
|
+
}
|
|
62
|
+
async pollReview(auditLogId, options = {}) {
|
|
63
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
64
|
+
const intervalMs = options.intervalMs ?? 2e3;
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
|
+
while (Date.now() < deadline) {
|
|
67
|
+
const status = await this.getReviewStatus(auditLogId);
|
|
68
|
+
if (status.decision !== "REVIEW") {
|
|
69
|
+
return status;
|
|
70
|
+
}
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
72
|
+
}
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Review timed out after ${timeoutMs}ms for auditLogId: ${auditLogId}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
async guardWithReview(action, params, callback, options = {}) {
|
|
78
|
+
const result = await this.check(action, params, options);
|
|
79
|
+
if (result.verdict.decision === "ALLOW") {
|
|
80
|
+
return await callback();
|
|
81
|
+
}
|
|
82
|
+
if (result.verdict.decision === "BLOCK") {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Action '${action}' blocked by Citadel: ${result.verdict.reason}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (options.onReviewPending) {
|
|
88
|
+
options.onReviewPending(result.auditLogId);
|
|
89
|
+
}
|
|
90
|
+
const reviewed = await this.pollReview(result.auditLogId, {
|
|
91
|
+
timeoutMs: options.timeoutMs,
|
|
92
|
+
intervalMs: options.pollIntervalMs
|
|
93
|
+
});
|
|
94
|
+
if (reviewed.decision === "ALLOW") {
|
|
95
|
+
return await callback();
|
|
96
|
+
}
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Action '${action}' rejected during human review: ${reviewed.reviewComment ?? "Rejected"}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/sdk/tools.ts
|
|
104
|
+
function wrapTool(client, tool) {
|
|
105
|
+
return {
|
|
106
|
+
...tool,
|
|
107
|
+
execute: async (args) => {
|
|
108
|
+
return await client.guard(
|
|
109
|
+
tool.name,
|
|
110
|
+
args,
|
|
111
|
+
() => tool.execute(args),
|
|
112
|
+
tool.options
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function wrapToolWithReview(client, tool, reviewOptions) {
|
|
118
|
+
return {
|
|
119
|
+
...tool,
|
|
120
|
+
execute: async (args) => {
|
|
121
|
+
return await client.guardWithReview(
|
|
122
|
+
tool.name,
|
|
123
|
+
args,
|
|
124
|
+
() => tool.execute(args),
|
|
125
|
+
{
|
|
126
|
+
...tool.options,
|
|
127
|
+
...reviewOptions
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/engine/semantic.ts
|
|
135
|
+
var INJECTION_PATTERNS = [
|
|
136
|
+
{
|
|
137
|
+
regex: /(?:ignore|disregard|forget|bypass|override)\s+(?:all\s+)?(?:previous|prior|existing|above)\s+(?:instructions|rules|guidelines|directions|constraints)/i,
|
|
138
|
+
description: "Direct prompt instruction override",
|
|
139
|
+
score: 95
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
regex: /(?:you\s+are\s+now|act\s+as|roleplay\s+as)\s+(?:an?\s+)?(?:unfiltered|unrestricted|jailbroken|evil|dan|developer\s+mode)/i,
|
|
143
|
+
description: "Adversarial persona / DAN jailbreak attempt",
|
|
144
|
+
score: 90
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
regex: /(?:reveal|output|print|display|leak)\s+(?:the\s+)?(?:system\s+prompt|initial\s+prompt|core\s+instructions|developer\s+instructions)/i,
|
|
148
|
+
description: "System prompt extraction attempt",
|
|
149
|
+
score: 85
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
regex: /(?:\[INST\]|\[\/INST\]|<<SYS>>|<\/SYS>|<\|im_start\|>|<\|im_end\|>)/i,
|
|
153
|
+
description: "Model token delimiter injection",
|
|
154
|
+
score: 90
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
var CREDENTIAL_PATTERNS = [
|
|
158
|
+
{
|
|
159
|
+
regex: /(?:sk-[a-zA-Z0-9-_]{20,}|ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36})/i,
|
|
160
|
+
description: "Exposed API token (OpenAI / GitHub)",
|
|
161
|
+
score: 95
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
165
|
+
description: "AWS Access Key ID",
|
|
166
|
+
score: 90
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
|
|
170
|
+
description: "Exposed Private Key block",
|
|
171
|
+
score: 99
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
function extractStrings(obj) {
|
|
175
|
+
const strings = [];
|
|
176
|
+
function recurse(value) {
|
|
177
|
+
if (typeof value === "string") {
|
|
178
|
+
strings.push(value);
|
|
179
|
+
} else if (Array.isArray(value)) {
|
|
180
|
+
for (const item of value) recurse(item);
|
|
181
|
+
} else if (value !== null && typeof value === "object") {
|
|
182
|
+
for (const val of Object.values(value)) {
|
|
183
|
+
recurse(val);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
recurse(obj);
|
|
188
|
+
return strings;
|
|
189
|
+
}
|
|
190
|
+
function scanPayloadSemantics(params) {
|
|
191
|
+
const stringsToScan = extractStrings(params);
|
|
192
|
+
const threats = [];
|
|
193
|
+
for (const text of stringsToScan) {
|
|
194
|
+
for (const rule of INJECTION_PATTERNS) {
|
|
195
|
+
if (rule.regex.test(text)) {
|
|
196
|
+
threats.push({
|
|
197
|
+
type: "PROMPT_INJECTION",
|
|
198
|
+
confidence: rule.score / 100,
|
|
199
|
+
reason: rule.description,
|
|
200
|
+
matchedPattern: rule.regex.source
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const rule of CREDENTIAL_PATTERNS) {
|
|
205
|
+
if (rule.regex.test(text)) {
|
|
206
|
+
threats.push({
|
|
207
|
+
type: "CREDENTIAL_LEAK",
|
|
208
|
+
confidence: rule.score / 100,
|
|
209
|
+
reason: rule.description,
|
|
210
|
+
matchedPattern: rule.regex.source
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const maxScore = threats.length > 0 ? Math.max(...threats.map((t) => t.confidence * 100)) : 0;
|
|
216
|
+
return {
|
|
217
|
+
flagged: threats.length > 0,
|
|
218
|
+
threats,
|
|
219
|
+
maxRiskScore: Math.round(maxScore)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/engine/match.ts
|
|
224
|
+
function extractField(obj, path) {
|
|
225
|
+
const parts = path.split(".");
|
|
226
|
+
let current = obj;
|
|
227
|
+
for (const part of parts) {
|
|
228
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
current = current[part];
|
|
232
|
+
}
|
|
233
|
+
return current;
|
|
234
|
+
}
|
|
235
|
+
function evaluateCondition(actual, operator, target) {
|
|
236
|
+
if (actual === void 0 || actual === null) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
switch (operator) {
|
|
240
|
+
case "EQUALS":
|
|
241
|
+
return actual === target;
|
|
242
|
+
case "NOT_EQUALS":
|
|
243
|
+
return actual !== target;
|
|
244
|
+
case "GREATER_THAN":
|
|
245
|
+
return typeof actual === "number" && typeof target === "number" && actual > target;
|
|
246
|
+
case "LESS_THAN":
|
|
247
|
+
return typeof actual === "number" && typeof target === "number" && actual < target;
|
|
248
|
+
case "IN":
|
|
249
|
+
return Array.isArray(target) && target.includes(actual);
|
|
250
|
+
case "NOT_IN":
|
|
251
|
+
return Array.isArray(target) && !target.includes(actual);
|
|
252
|
+
case "CONTAINS":
|
|
253
|
+
if (typeof actual === "string" && typeof target === "string") {
|
|
254
|
+
return actual.includes(target);
|
|
255
|
+
}
|
|
256
|
+
if (Array.isArray(actual)) {
|
|
257
|
+
return actual.includes(target);
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
case "REGEX_MATCH":
|
|
261
|
+
if (typeof actual !== "string" || typeof target !== "string") {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
return new RegExp(target).test(actual);
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function matchesActionPattern(pattern, action) {
|
|
274
|
+
if (pattern === "*" || pattern === action) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (pattern.endsWith("*")) {
|
|
278
|
+
const prefix = pattern.slice(0, -1);
|
|
279
|
+
return action.startsWith(prefix);
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
function evaluateRule(payload, rule) {
|
|
284
|
+
const targetObject = {
|
|
285
|
+
action: payload.action,
|
|
286
|
+
params: payload.params,
|
|
287
|
+
context: payload.context,
|
|
288
|
+
timestamp: payload.timestamp,
|
|
289
|
+
metadata: payload.metadata ?? {}
|
|
290
|
+
};
|
|
291
|
+
const actualValue = extractField(targetObject, rule.field);
|
|
292
|
+
return evaluateCondition(actualValue, rule.operator, rule.value);
|
|
293
|
+
}
|
|
294
|
+
function calculateRisk(decision, matchedCount, semanticScore = 0) {
|
|
295
|
+
if (decision === "BLOCK") {
|
|
296
|
+
return { riskScore: Math.max(95, semanticScore), riskLevel: "CRITICAL" };
|
|
297
|
+
}
|
|
298
|
+
if (decision === "REVIEW") {
|
|
299
|
+
return { riskScore: Math.max(65, semanticScore), riskLevel: "HIGH" };
|
|
300
|
+
}
|
|
301
|
+
if (matchedCount > 0) {
|
|
302
|
+
return { riskScore: 25, riskLevel: "MEDIUM" };
|
|
303
|
+
}
|
|
304
|
+
return { riskScore: 5, riskLevel: "LOW" };
|
|
305
|
+
}
|
|
306
|
+
function evaluatePolicies(payload, policies) {
|
|
307
|
+
const startTime = Date.now();
|
|
308
|
+
const traces = [];
|
|
309
|
+
const activePolicies = policies.filter((p) => p.isActive && p.tenantId === payload.context.tenantId).sort((a, b) => b.priority - a.priority);
|
|
310
|
+
let finalDecision = "ALLOW";
|
|
311
|
+
let primaryReason = "No blocking or review policies triggered";
|
|
312
|
+
let pipeline = "DETERMINISTIC";
|
|
313
|
+
for (const policy of activePolicies) {
|
|
314
|
+
if (!matchesActionPattern(policy.actionPattern, payload.action)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const rulesMatched = policy.rules.length > 0 && policy.rules.every((rule) => evaluateRule(payload, rule));
|
|
318
|
+
if (rulesMatched) {
|
|
319
|
+
traces.push({
|
|
320
|
+
policyId: policy.id,
|
|
321
|
+
policyName: policy.name,
|
|
322
|
+
matched: true,
|
|
323
|
+
effect: policy.effect
|
|
324
|
+
});
|
|
325
|
+
if (policy.effect === "BLOCK") {
|
|
326
|
+
finalDecision = "BLOCK";
|
|
327
|
+
primaryReason = `Blocked by policy: ${policy.name}`;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
if (policy.effect === "REVIEW") {
|
|
331
|
+
finalDecision = "REVIEW";
|
|
332
|
+
primaryReason = `Review required by policy: ${policy.name}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
let semanticScore = 0;
|
|
337
|
+
if (finalDecision !== "BLOCK") {
|
|
338
|
+
const semanticResult = scanPayloadSemantics(payload.params);
|
|
339
|
+
if (semanticResult.flagged) {
|
|
340
|
+
pipeline = traces.length > 0 ? "HYBRID" : "AI_GUARD";
|
|
341
|
+
semanticScore = semanticResult.maxRiskScore;
|
|
342
|
+
finalDecision = "BLOCK";
|
|
343
|
+
const threatReasons = semanticResult.threats.map((t) => t.reason).join(", ");
|
|
344
|
+
primaryReason = `AI Guard blocked threat: ${threatReasons}`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const { riskScore, riskLevel } = calculateRisk(finalDecision, traces.length, semanticScore);
|
|
348
|
+
const latencyMs = Date.now() - startTime;
|
|
349
|
+
return {
|
|
350
|
+
verdictId: `vrd_${Math.random().toString(36).slice(2, 11)}`,
|
|
351
|
+
decision: finalDecision,
|
|
352
|
+
reason: primaryReason,
|
|
353
|
+
riskScore,
|
|
354
|
+
riskLevel,
|
|
355
|
+
evaluationPipeline: pipeline,
|
|
356
|
+
matchedPolicies: traces,
|
|
357
|
+
executedAt: startTime,
|
|
358
|
+
latencyMs
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export {
|
|
362
|
+
CitadelClient,
|
|
363
|
+
evaluatePolicies,
|
|
364
|
+
scanPayloadSemantics,
|
|
365
|
+
wrapTool,
|
|
366
|
+
wrapToolWithReview
|
|
367
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "citadel0",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Programmatic Security Gateway & Policy Engine for AI Agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
26
|
+
"prepack": "pnpm run build",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"ai",
|
|
31
|
+
"security",
|
|
32
|
+
"guardrails",
|
|
33
|
+
"agents",
|
|
34
|
+
"prompt-injection",
|
|
35
|
+
"policy-engine"
|
|
36
|
+
],
|
|
37
|
+
"author": "Moulick Bose",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"packageManager": "pnpm@12.4.2",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^26.6.1",
|
|
42
|
+
"tsup": "^8.4.0",
|
|
43
|
+
"tsx": "^4.23.13",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"convex": "^1.46.0"
|
|
48
|
+
}
|
|
49
|
+
}
|