@iris-eval/mcp-server 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit-log-reader.js +2 -2
- package/dist/config/defaults.js +3 -3
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +14 -5
- package/dist/custom-rule-store.js +81 -33
- package/dist/dashboard/assets/{index-CIKsbEhq.js → index-ChcHJDDJ.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/server.js +44 -2
- package/dist/eval/engine.js +25 -10
- package/dist/eval/rules/regex-budget.d.ts +5 -0
- package/dist/eval/rules/regex-budget.js +0 -0
- package/dist/eval/rules/safety.js +41 -5
- package/dist/index.js +10 -1
- package/dist/middleware/rebinding-guard.d.ts +21 -0
- package/dist/middleware/rebinding-guard.js +77 -0
- package/dist/preferences.js +5 -10
- package/dist/storage/migrations/005-normalize-created-at.d.ts +3 -0
- package/dist/storage/migrations/005-normalize-created-at.js +34 -0
- package/dist/storage/migrations/index.js +8 -1
- package/dist/storage/sqlite-adapter.js +28 -4
- package/dist/types/config.d.ts +7 -0
- package/dist/utils/iris-home.d.ts +1 -0
- package/dist/utils/iris-home.js +21 -0
- package/dist/utils/write-atomic.d.ts +1 -0
- package/dist/utils/write-atomic.js +64 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/audit-log-reader.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { readFileSync, existsSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
-
import {
|
|
15
|
+
import { irisHome } from './utils/iris-home.js';
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
const AUDIT_ACTIONS = ['rule.deploy', 'rule.delete', 'rule.toggle', 'rule.update'];
|
|
18
18
|
const EntrySchema = z.object({
|
|
@@ -31,7 +31,7 @@ const EntrySchema = z.object({
|
|
|
31
31
|
details: z.record(z.string(), z.unknown()).optional(),
|
|
32
32
|
});
|
|
33
33
|
function defaultAuditPath() {
|
|
34
|
-
return join(
|
|
34
|
+
return join(irisHome(), 'audit.log');
|
|
35
35
|
}
|
|
36
36
|
export function readAuditLog(opts) {
|
|
37
37
|
const filePath = opts?.filePath ?? defaultAuditPath();
|
package/dist/config/defaults.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
const irisHome = join(homedir(), '.iris');
|
|
3
|
+
import { irisHome } from '../utils/iris-home.js';
|
|
5
4
|
// Read version from package.json to avoid hardcoded drift
|
|
6
5
|
let pkgVersion = '0.1.8';
|
|
7
6
|
try {
|
|
@@ -17,7 +16,7 @@ export const PKG_VERSION = pkgVersion;
|
|
|
17
16
|
export const defaultConfig = {
|
|
18
17
|
storage: {
|
|
19
18
|
type: 'sqlite',
|
|
20
|
-
path: join(irisHome, 'iris.db'),
|
|
19
|
+
path: join(irisHome(), 'iris.db'),
|
|
21
20
|
},
|
|
22
21
|
server: {
|
|
23
22
|
name: 'iris-eval',
|
|
@@ -31,6 +30,7 @@ export const defaultConfig = {
|
|
|
31
30
|
dashboard: {
|
|
32
31
|
enabled: false,
|
|
33
32
|
port: 6920,
|
|
33
|
+
host: '127.0.0.1',
|
|
34
34
|
},
|
|
35
35
|
eval: {
|
|
36
36
|
defaultThreshold: 0.7,
|
package/dist/config/index.d.ts
CHANGED
package/dist/config/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { defaultConfig } from './defaults.js';
|
|
4
|
+
import { irisHome } from '../utils/iris-home.js';
|
|
5
5
|
function deepMerge(target, source) {
|
|
6
6
|
const result = { ...target };
|
|
7
7
|
for (const key of Object.keys(source)) {
|
|
@@ -71,6 +71,12 @@ function loadEnvVars() {
|
|
|
71
71
|
port: parsePortEnv(process.env.IRIS_DASHBOARD_PORT, 'IRIS_DASHBOARD_PORT'),
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
if (process.env.IRIS_DASHBOARD_HOST) {
|
|
75
|
+
config.dashboard = {
|
|
76
|
+
...config.dashboard,
|
|
77
|
+
host: process.env.IRIS_DASHBOARD_HOST,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
74
80
|
if (process.env.IRIS_API_KEY) {
|
|
75
81
|
config.security = { ...config.security, apiKey: process.env.IRIS_API_KEY };
|
|
76
82
|
}
|
|
@@ -99,17 +105,20 @@ function cliArgsToConfig(args) {
|
|
|
99
105
|
if (args.dashboardPort) {
|
|
100
106
|
config.dashboard = { ...config.dashboard, port: args.dashboardPort };
|
|
101
107
|
}
|
|
108
|
+
if (args.dashboardHost) {
|
|
109
|
+
config.dashboard = { ...config.dashboard, host: args.dashboardHost };
|
|
110
|
+
}
|
|
102
111
|
if (args.apiKey) {
|
|
103
112
|
config.security = { ...config.security, apiKey: args.apiKey };
|
|
104
113
|
}
|
|
105
114
|
return config;
|
|
106
115
|
}
|
|
107
116
|
export function loadConfig(cliArgs) {
|
|
108
|
-
const
|
|
109
|
-
if (!existsSync(
|
|
110
|
-
mkdirSync(
|
|
117
|
+
const home = irisHome();
|
|
118
|
+
if (!existsSync(home)) {
|
|
119
|
+
mkdirSync(home, { recursive: true });
|
|
111
120
|
}
|
|
112
|
-
const configPath = cliArgs?.config ?? join(
|
|
121
|
+
const configPath = cliArgs?.config ?? join(home, 'config.json');
|
|
113
122
|
const fileConfig = loadConfigFile(configPath);
|
|
114
123
|
const envConfig = loadEnvVars();
|
|
115
124
|
const argsConfig = cliArgs ? cliArgsToConfig(cliArgs) : {};
|
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
* For now we use atomic write-via-rename so a crashed write doesn't
|
|
24
24
|
* leave a half-file.
|
|
25
25
|
*/
|
|
26
|
-
import { mkdirSync, readFileSync,
|
|
26
|
+
import { mkdirSync, readFileSync, existsSync, appendFileSync } from 'node:fs';
|
|
27
|
+
import { writeAtomic } from './utils/write-atomic.js';
|
|
28
|
+
import { irisHome } from './utils/iris-home.js';
|
|
27
29
|
import { join, dirname } from 'node:path';
|
|
28
|
-
import { homedir } from 'node:os';
|
|
29
30
|
import { randomBytes } from 'node:crypto';
|
|
30
31
|
import { z } from 'zod';
|
|
31
32
|
import isSafeRegex from 'safe-regex2';
|
|
33
|
+
import { regexBacktrackingBudgetExceeded } from './eval/rules/regex-budget.js';
|
|
32
34
|
import { CUSTOM_RULE_CONFIG_KEYS, readNumericConfig, describeKeys } from './eval/rules/config-keys.js';
|
|
33
35
|
import { LOCAL_TENANT } from './types/tenant.js';
|
|
34
36
|
const SEVERITY_VALUES = ['low', 'medium', 'high', 'critical'];
|
|
@@ -121,6 +123,21 @@ const DefinitionSchema = z
|
|
|
121
123
|
path: ['config', 'pattern'],
|
|
122
124
|
message: 'Regex pattern rejected: potentially unsafe (catastrophic backtracking)',
|
|
123
125
|
});
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
// safe-regex2 is a star-height heuristic — it catches EXPONENTIAL
|
|
129
|
+
// blowup only. Polynomial patterns pass it: a*a*a*a*a*b is judged
|
|
130
|
+
// safe and takes 156ms on 40 characters. Measure what the static
|
|
131
|
+
// check cannot see.
|
|
132
|
+
{
|
|
133
|
+
const budgetIssue = regexBacktrackingBudgetExceeded(stripped, typeof config.flags === 'string' ? config.flags : '');
|
|
134
|
+
if (budgetIssue) {
|
|
135
|
+
ctx.addIssue({
|
|
136
|
+
code: z.ZodIssueCode.custom,
|
|
137
|
+
path: ['config', 'pattern'],
|
|
138
|
+
message: budgetIssue,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
124
141
|
}
|
|
125
142
|
break;
|
|
126
143
|
}
|
|
@@ -167,26 +184,22 @@ const DeployedRuleSchema = z.object({
|
|
|
167
184
|
sourceMomentId: z.string().optional(),
|
|
168
185
|
version: z.number().int().positive(),
|
|
169
186
|
});
|
|
170
|
-
const FileSchema = z.object({
|
|
171
|
-
version: z.literal(1),
|
|
172
|
-
rules: z.array(DeployedRuleSchema),
|
|
173
|
-
});
|
|
174
187
|
/**
|
|
175
188
|
* Default file path for a tenant. LOCAL_TENANT keeps the v0.4 path
|
|
176
189
|
* (zero migration); others get a per-tenant suffix.
|
|
177
190
|
*/
|
|
178
191
|
function defaultPathFor(tenantId) {
|
|
179
192
|
if (tenantId === LOCAL_TENANT) {
|
|
180
|
-
return join(
|
|
193
|
+
return join(irisHome(), 'custom-rules.json');
|
|
181
194
|
}
|
|
182
195
|
// Sanitize tenant id for filesystem safety. TenantId is branded but
|
|
183
196
|
// could in principle contain odd chars on Cloud — limit to a known-safe
|
|
184
197
|
// alphabet so we never write outside the .iris directory.
|
|
185
198
|
const safe = String(tenantId).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
186
|
-
return join(
|
|
199
|
+
return join(irisHome(), `custom-rules-${safe}.json`);
|
|
187
200
|
}
|
|
188
201
|
function defaultAuditPath() {
|
|
189
|
-
return join(
|
|
202
|
+
return join(irisHome(), 'audit.log');
|
|
190
203
|
}
|
|
191
204
|
function generateRuleId() {
|
|
192
205
|
return `rule-${randomBytes(4).toString('hex')}`;
|
|
@@ -201,45 +214,80 @@ function appendAudit(auditPath, entry) {
|
|
|
201
214
|
// still succeeds; the operator just loses the audit trail.
|
|
202
215
|
}
|
|
203
216
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
217
|
+
/*
|
|
218
|
+
* Read leniently, one rule at a time.
|
|
219
|
+
*
|
|
220
|
+
* This used to validate the whole array with a single safeParse and return
|
|
221
|
+
* [] if ANY element failed. The empty result was then cached, and the next
|
|
222
|
+
* deploy/delete/toggle called persist(), which wrote {version:1, rules:[]}
|
|
223
|
+
* over the file — permanently destroying every valid rule alongside the
|
|
224
|
+
* bad one. The old comment ("do NOT overwrite the file") described an
|
|
225
|
+
* intent the write path did not honour.
|
|
226
|
+
*
|
|
227
|
+
* It was reachable, not theoretical: DefinitionSchema's superRefine now
|
|
228
|
+
* runs on READ as well as WRITE, and eval/rules/custom.ts notes that rules
|
|
229
|
+
* predating that validation — e.g. {type:'min_length', config:{}} — are
|
|
230
|
+
* already sitting in users' files.
|
|
231
|
+
*/
|
|
210
232
|
function loadRulesFromDisk(rulesPath) {
|
|
211
233
|
if (!existsSync(rulesPath))
|
|
212
|
-
return [];
|
|
234
|
+
return { rules: [], quarantined: [], readable: true };
|
|
235
|
+
let parsedJson;
|
|
213
236
|
try {
|
|
214
|
-
|
|
215
|
-
const parsed = FileSchema.safeParse(JSON.parse(raw));
|
|
216
|
-
if (parsed.success)
|
|
217
|
-
return parsed.data.rules;
|
|
218
|
-
// Malformed: leave rules empty; do NOT overwrite the file.
|
|
219
|
-
return [];
|
|
237
|
+
parsedJson = JSON.parse(readFileSync(rulesPath, 'utf-8'));
|
|
220
238
|
}
|
|
221
239
|
catch {
|
|
222
|
-
|
|
223
|
-
|
|
240
|
+
return { rules: [], quarantined: [], readable: false };
|
|
241
|
+
}
|
|
242
|
+
const envelope = z.object({ rules: z.array(z.unknown()).optional() }).safeParse(parsedJson);
|
|
243
|
+
if (!envelope.success)
|
|
244
|
+
return { rules: [], quarantined: [], readable: false };
|
|
245
|
+
const rules = [];
|
|
246
|
+
const quarantined = [];
|
|
247
|
+
for (const entry of envelope.data.rules ?? []) {
|
|
248
|
+
const rule = DeployedRuleSchema.safeParse(entry);
|
|
249
|
+
if (rule.success)
|
|
250
|
+
rules.push(rule.data);
|
|
251
|
+
else
|
|
252
|
+
quarantined.push(entry);
|
|
224
253
|
}
|
|
254
|
+
return { rules, quarantined, readable: true };
|
|
225
255
|
}
|
|
226
256
|
export function createCustomRuleStore(opts) {
|
|
227
257
|
const pathFor = opts?.pathFor ?? defaultPathFor;
|
|
228
258
|
const auditPath = opts?.auditPath ?? defaultAuditPath();
|
|
229
259
|
// In-memory cache keyed by tenant. Lazy-loaded on first access per
|
|
230
260
|
// tenant; subsequent calls hit the cache.
|
|
231
|
-
const
|
|
232
|
-
function
|
|
233
|
-
let
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
const tenantState = new Map();
|
|
262
|
+
function state(tenantId) {
|
|
263
|
+
let loaded = tenantState.get(tenantId);
|
|
264
|
+
if (loaded === undefined) {
|
|
265
|
+
loaded = loadRulesFromDisk(pathFor(tenantId));
|
|
266
|
+
tenantState.set(tenantId, loaded);
|
|
237
267
|
}
|
|
238
|
-
return
|
|
268
|
+
return loaded;
|
|
269
|
+
}
|
|
270
|
+
function load(tenantId) {
|
|
271
|
+
return state(tenantId).rules;
|
|
239
272
|
}
|
|
240
273
|
function persist(tenantId) {
|
|
241
|
-
const
|
|
242
|
-
|
|
274
|
+
const loaded = state(tenantId);
|
|
275
|
+
if (!loaded.readable) {
|
|
276
|
+
/*
|
|
277
|
+
* The file exists but never parsed. Overwriting it would replace
|
|
278
|
+
* content we could not read — exactly the data loss this store used
|
|
279
|
+
* to cause silently. Fail loudly so the caller surfaces a 500 and
|
|
280
|
+
* the operator can fix or move the file.
|
|
281
|
+
*/
|
|
282
|
+
throw new Error(`Refusing to write ${pathFor(tenantId)}: the existing file could not be parsed. ` +
|
|
283
|
+
`Fix or move it, then retry — writing now would destroy its contents.`);
|
|
284
|
+
}
|
|
285
|
+
// Quarantined entries ride along untouched so a deploy never deletes
|
|
286
|
+
// rules this version could not validate.
|
|
287
|
+
const file = {
|
|
288
|
+
version: 1,
|
|
289
|
+
rules: [...loaded.rules, ...loaded.quarantined],
|
|
290
|
+
};
|
|
243
291
|
writeAtomic(pathFor(tenantId), JSON.stringify(file, null, 2));
|
|
244
292
|
}
|
|
245
293
|
return {
|