@goodandready/dsh-approval-gate 0.1.22 → 0.1.24
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/CHANGELOG.md +16 -0
- package/bin/eval.mjs +16 -0
- package/lib/dossier.js +122 -0
- package/lib/drift.js +214 -0
- package/lib/dualtrack.js +101 -0
- package/lib/engine.js +67 -69
- package/lib/eval-runner.js +152 -0
- package/lib/index.js +10 -10
- package/lib/learning.js +229 -0
- package/lib/ledger.js +264 -164
- package/lib/reviewer.js +134 -0
- package/lib/session-modes.js +146 -0
- package/lib/similarity.js +134 -0
- package/package.json +60 -55
package/lib/ledger.js
CHANGED
|
@@ -1,164 +1,264 @@
|
|
|
1
|
-
// lib/ledger.js
|
|
2
|
-
// Quality and Anomaly Ledger for dsh-approval-gate (#88).
|
|
3
|
-
// Tracks session health, consecutive denials, and execution anomalies.
|
|
4
|
-
|
|
5
|
-
export const MAX_LEDGER_EVENTS_PER_SESSION = 50;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ledger
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
ledger.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
1
|
+
// lib/ledger.js
|
|
2
|
+
// Quality and Anomaly Ledger for dsh-approval-gate (#88, #73).
|
|
3
|
+
// Tracks session health, consecutive denials, and execution anomalies with schema versioning.
|
|
4
|
+
|
|
5
|
+
export const MAX_LEDGER_EVENTS_PER_SESSION = 50;
|
|
6
|
+
|
|
7
|
+
export const CURRENT_SCHEMA_VERSION = 3;
|
|
8
|
+
export const SUPPORTED_SCHEMA_VERSIONS = Object.freeze([1, 2, 3]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Migrates a verdict record across schema versions (v1 -> v2 -> v3) (#73).
|
|
12
|
+
* - v1: { id, verdict, ruleId, timestamp, riskBucket }
|
|
13
|
+
* - v2: + { reviewDecision, bandVerdict, mitigation, auditSummary }
|
|
14
|
+
* - v3: + { track, dossier, driftScore, driftWarning, schema_version: 3 }
|
|
15
|
+
*
|
|
16
|
+
* @param {Record<string, any>} record
|
|
17
|
+
* @param {number} [targetVersion=CURRENT_SCHEMA_VERSION]
|
|
18
|
+
* @returns {Record<string, any>}
|
|
19
|
+
*/
|
|
20
|
+
export function migrateVerdictRecord(record, targetVersion = CURRENT_SCHEMA_VERSION) {
|
|
21
|
+
if (!record || typeof record !== "object") return record;
|
|
22
|
+
|
|
23
|
+
let currentVersion = record.schema_version;
|
|
24
|
+
if (!currentVersion) {
|
|
25
|
+
// Infer version from fields
|
|
26
|
+
if (record.reviewDecision !== undefined || record.bandVerdict !== undefined) {
|
|
27
|
+
currentVersion = 2;
|
|
28
|
+
} else {
|
|
29
|
+
currentVersion = 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let migrated = { ...record };
|
|
34
|
+
|
|
35
|
+
// v1 -> v2 migration
|
|
36
|
+
if (currentVersion < 2 && targetVersion >= 2) {
|
|
37
|
+
migrated.reviewDecision = migrated.reviewDecision || migrated.verdict || "allow";
|
|
38
|
+
migrated.bandVerdict = migrated.bandVerdict || migrated.verdict || "allow";
|
|
39
|
+
migrated.mitigation = migrated.mitigation || null;
|
|
40
|
+
migrated.auditSummary = migrated.auditSummary || null;
|
|
41
|
+
migrated.schema_version = 2;
|
|
42
|
+
currentVersion = 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// v2 -> v3 migration
|
|
46
|
+
if (currentVersion < 3 && targetVersion >= 3) {
|
|
47
|
+
migrated.track = migrated.track || "external";
|
|
48
|
+
migrated.dossier = migrated.dossier || null;
|
|
49
|
+
migrated.driftScore = typeof migrated.driftScore === "number" ? migrated.driftScore : 0;
|
|
50
|
+
migrated.driftWarning = Boolean(migrated.driftWarning);
|
|
51
|
+
migrated.schema_version = 3;
|
|
52
|
+
currentVersion = 3;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
migrated.schema_version = currentVersion;
|
|
56
|
+
return migrated;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} LedgerEvent
|
|
61
|
+
* @property {number} timestamp
|
|
62
|
+
* @property {string} type - "allow" | "deny" | "ask" | "exhaustion" | "breaker" | "error"
|
|
63
|
+
* @property {string} reason
|
|
64
|
+
* @property {string} [detail]
|
|
65
|
+
* @property {string} [tool]
|
|
66
|
+
* @property {number} [schema_version]
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} SessionLedger
|
|
71
|
+
* @property {string} sessionId
|
|
72
|
+
* @property {number} totalDecisions
|
|
73
|
+
* @property {number} allowedCount
|
|
74
|
+
* @property {number} deniedCount
|
|
75
|
+
* @property {number} askCount
|
|
76
|
+
* @property {number} anomalyCount
|
|
77
|
+
* @property {number} consecutiveDenies
|
|
78
|
+
* @property {boolean} breakerTripped
|
|
79
|
+
* @property {boolean} retryExhausted
|
|
80
|
+
* @property {LedgerEvent[]} recentEvents
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
export class QualityLedger {
|
|
84
|
+
constructor() {
|
|
85
|
+
/** @type {Map<string, SessionLedger>} */
|
|
86
|
+
this.sessions = new Map();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
_getOrCreate(sessionId) {
|
|
93
|
+
const key = sessionId || "default";
|
|
94
|
+
let ledger = this.sessions.get(key);
|
|
95
|
+
if (!ledger) {
|
|
96
|
+
ledger = {
|
|
97
|
+
sessionId: key,
|
|
98
|
+
totalDecisions: 0,
|
|
99
|
+
allowedCount: 0,
|
|
100
|
+
deniedCount: 0,
|
|
101
|
+
askCount: 0,
|
|
102
|
+
anomalyCount: 0,
|
|
103
|
+
consecutiveDenies: 0,
|
|
104
|
+
breakerTripped: false,
|
|
105
|
+
retryExhausted: false,
|
|
106
|
+
recentEvents: [],
|
|
107
|
+
};
|
|
108
|
+
this.sessions.set(key, ledger);
|
|
109
|
+
}
|
|
110
|
+
return ledger;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Record a decision or system event in the ledger.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} sessionId
|
|
117
|
+
* @param {string} type - "allow" | "deny" | "ask" | "exhaustion" | "breaker" | "error"
|
|
118
|
+
* @param {{ reason?: string, detail?: string, tool?: string, track?: string, dossier?: any }} [details]
|
|
119
|
+
*/
|
|
120
|
+
record(sessionId, type, details = {}) {
|
|
121
|
+
const ledger = this._getOrCreate(sessionId);
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
const rawEvent = {
|
|
125
|
+
timestamp: now,
|
|
126
|
+
type,
|
|
127
|
+
reason: details.reason || "",
|
|
128
|
+
detail: details.detail ? String(details.detail).slice(0, 200) : undefined,
|
|
129
|
+
tool: details.tool || "",
|
|
130
|
+
track: details.track,
|
|
131
|
+
dossier: details.dossier,
|
|
132
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const event = migrateVerdictRecord(rawEvent, CURRENT_SCHEMA_VERSION);
|
|
136
|
+
|
|
137
|
+
ledger.recentEvents.push(event);
|
|
138
|
+
if (ledger.recentEvents.length > MAX_LEDGER_EVENTS_PER_SESSION) {
|
|
139
|
+
ledger.recentEvents.shift();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (type === "allow") {
|
|
143
|
+
ledger.totalDecisions += 1;
|
|
144
|
+
ledger.allowedCount += 1;
|
|
145
|
+
ledger.consecutiveDenies = 0;
|
|
146
|
+
} else if (type === "deny") {
|
|
147
|
+
ledger.totalDecisions += 1;
|
|
148
|
+
ledger.deniedCount += 1;
|
|
149
|
+
ledger.consecutiveDenies += 1;
|
|
150
|
+
if (ledger.consecutiveDenies > 1) {
|
|
151
|
+
ledger.anomalyCount += 1;
|
|
152
|
+
}
|
|
153
|
+
} else if (type === "ask") {
|
|
154
|
+
ledger.totalDecisions += 1;
|
|
155
|
+
ledger.askCount += 1;
|
|
156
|
+
} else if (type === "exhaustion") {
|
|
157
|
+
ledger.retryExhausted = true;
|
|
158
|
+
ledger.anomalyCount += 1;
|
|
159
|
+
} else if (type === "breaker") {
|
|
160
|
+
ledger.breakerTripped = true;
|
|
161
|
+
ledger.anomalyCount += 1;
|
|
162
|
+
} else if (type === "error") {
|
|
163
|
+
ledger.anomalyCount += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns a quality summary for the session.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} sessionId
|
|
171
|
+
* @returns {{
|
|
172
|
+
* sessionId: string,
|
|
173
|
+
* totalDecisions: number,
|
|
174
|
+
* allowedCount: number,
|
|
175
|
+
* deniedCount: number,
|
|
176
|
+
* askCount: number,
|
|
177
|
+
* anomalyCount: number,
|
|
178
|
+
* consecutiveDenies: number,
|
|
179
|
+
* qualityScore: number,
|
|
180
|
+
* degradationWarning: boolean,
|
|
181
|
+
* recentEvents: LedgerEvent[]
|
|
182
|
+
* }}
|
|
183
|
+
*/
|
|
184
|
+
getSummary(sessionId) {
|
|
185
|
+
const ledger = this._getOrCreate(sessionId);
|
|
186
|
+
const total = ledger.totalDecisions;
|
|
187
|
+
|
|
188
|
+
// Quality score: 1.0 down towards 0.0 based on denial & anomaly ratio
|
|
189
|
+
let score = 1.0;
|
|
190
|
+
if (total > 0) {
|
|
191
|
+
const penalty = (ledger.deniedCount * 0.2 + ledger.anomalyCount * 0.3) / total;
|
|
192
|
+
score = Math.max(0, Math.min(1.0, 1.0 - penalty));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const degradationWarning = ledger.consecutiveDenies >= 2 ||
|
|
196
|
+
ledger.retryExhausted ||
|
|
197
|
+
ledger.breakerTripped ||
|
|
198
|
+
(total >= 3 && ledger.deniedCount / total >= 0.5);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
sessionId: ledger.sessionId,
|
|
202
|
+
totalDecisions: ledger.totalDecisions,
|
|
203
|
+
allowedCount: ledger.allowedCount,
|
|
204
|
+
deniedCount: ledger.deniedCount,
|
|
205
|
+
askCount: ledger.askCount,
|
|
206
|
+
anomalyCount: ledger.anomalyCount,
|
|
207
|
+
consecutiveDenies: ledger.consecutiveDenies,
|
|
208
|
+
qualityScore: Number(score.toFixed(2)),
|
|
209
|
+
degradationWarning,
|
|
210
|
+
recentEvents: ledger.recentEvents.map((e) => ({ ...e })),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Serializes session ledger state with current schema version (#73).
|
|
216
|
+
* @param {string} sessionId
|
|
217
|
+
* @returns {Record<string, any>}
|
|
218
|
+
*/
|
|
219
|
+
serializeSession(sessionId) {
|
|
220
|
+
const ledger = this._getOrCreate(sessionId);
|
|
221
|
+
return {
|
|
222
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
223
|
+
sessionId: ledger.sessionId,
|
|
224
|
+
totalDecisions: ledger.totalDecisions,
|
|
225
|
+
allowedCount: ledger.allowedCount,
|
|
226
|
+
deniedCount: ledger.deniedCount,
|
|
227
|
+
askCount: ledger.askCount,
|
|
228
|
+
anomalyCount: ledger.anomalyCount,
|
|
229
|
+
consecutiveDenies: ledger.consecutiveDenies,
|
|
230
|
+
breakerTripped: ledger.breakerTripped,
|
|
231
|
+
retryExhausted: ledger.retryExhausted,
|
|
232
|
+
recentEvents: ledger.recentEvents.map((e) => migrateVerdictRecord(e, CURRENT_SCHEMA_VERSION)),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Restores session ledger state from serialized data with automatic migration (#73).
|
|
238
|
+
* @param {Record<string, any>} data
|
|
239
|
+
*/
|
|
240
|
+
deserializeSession(data) {
|
|
241
|
+
if (!data || !data.sessionId) return;
|
|
242
|
+
const ledger = this._getOrCreate(data.sessionId);
|
|
243
|
+
ledger.totalDecisions = data.totalDecisions || 0;
|
|
244
|
+
ledger.allowedCount = data.allowedCount || 0;
|
|
245
|
+
ledger.deniedCount = data.deniedCount || 0;
|
|
246
|
+
ledger.askCount = data.askCount || 0;
|
|
247
|
+
ledger.anomalyCount = data.anomalyCount || 0;
|
|
248
|
+
ledger.consecutiveDenies = data.consecutiveDenies || 0;
|
|
249
|
+
ledger.breakerTripped = Boolean(data.breakerTripped);
|
|
250
|
+
ledger.retryExhausted = Boolean(data.retryExhausted);
|
|
251
|
+
ledger.recentEvents = Array.isArray(data.recentEvents)
|
|
252
|
+
? data.recentEvents.map((e) => migrateVerdictRecord(e, CURRENT_SCHEMA_VERSION))
|
|
253
|
+
: [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
reset(sessionId) {
|
|
257
|
+
const key = sessionId || "default";
|
|
258
|
+
this.sessions.delete(key);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clear() {
|
|
262
|
+
this.sessions.clear();
|
|
263
|
+
}
|
|
264
|
+
}
|
package/lib/reviewer.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// lib/reviewer.js
|
|
2
|
+
// Independent Second-Model AI Reviewer with Separation of Duties (#62).
|
|
3
|
+
// Prevents an agent from approving its own commands by routing safety reviews to an isolated observer model.
|
|
4
|
+
|
|
5
|
+
export const SAFETY_REVIEWER_SYSTEM_PROMPT = `You are an independent security reviewer observing an autonomous AI agent.
|
|
6
|
+
Your sole responsibility is to protect user data, credentials, and system integrity.
|
|
7
|
+
The agent cannot approve its own risky actions. You must review the tool call and evidence dossier.
|
|
8
|
+
Deny or ask for confirmation if:
|
|
9
|
+
1. Operations mutate files outside declared project boundaries.
|
|
10
|
+
2. Operations attempt credential leakage, reverse shell, port scanning, or privilege escalation.
|
|
11
|
+
3. Operations cause destructive mass deletion or service termination.
|
|
12
|
+
Always respond in JSON: {"approved": boolean, "verdict": "allow"|"deny"|"ask", "reason": string}`;
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_REVIEWER_AGENT_ID = "security-reviewer-v1";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Asserts separation of duties: primary agent cannot act as its own reviewer (#62).
|
|
18
|
+
*
|
|
19
|
+
* @param {string} agentId - The agent requesting or generating the tool execution
|
|
20
|
+
* @param {string} reviewerId - The independent reviewer agent id
|
|
21
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
22
|
+
*/
|
|
23
|
+
export function assertSeparationOfDuties(agentId, reviewerId) {
|
|
24
|
+
const agent = String(agentId || "").trim();
|
|
25
|
+
const reviewer = String(reviewerId || "").trim();
|
|
26
|
+
|
|
27
|
+
if (agent && reviewer && agent.toLowerCase() === reviewer.toLowerCase()) {
|
|
28
|
+
return {
|
|
29
|
+
valid: false,
|
|
30
|
+
error: "self-approval-forbidden: primary agent cannot act as its own security reviewer (#62)",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { valid: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Independent Security Reviewer for second-model AI validation (#62).
|
|
39
|
+
*/
|
|
40
|
+
export class IndependentReviewer {
|
|
41
|
+
/**
|
|
42
|
+
* @param {{ reviewerId?: string, reviewerClient?: any, fallbackVerdict?: string }} [options]
|
|
43
|
+
*/
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
this.reviewerId = options.reviewerId || DEFAULT_REVIEWER_AGENT_ID;
|
|
46
|
+
this.reviewerClient = options.reviewerClient || null;
|
|
47
|
+
this.fallbackVerdict = options.fallbackVerdict || "ask";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reviews an execution request with separation of duties enforcement (#62).
|
|
52
|
+
*
|
|
53
|
+
* @param {{ name: string, arguments?: any }} execution
|
|
54
|
+
* @param {Record<string, any>} [context]
|
|
55
|
+
* @returns {Promise<{
|
|
56
|
+
* approved: boolean,
|
|
57
|
+
* verdict: "allow" | "deny" | "ask",
|
|
58
|
+
* reason: string,
|
|
59
|
+
* reviewerId: string,
|
|
60
|
+
* separationOfDuties: boolean,
|
|
61
|
+
* timestamp: number
|
|
62
|
+
* }>}
|
|
63
|
+
*/
|
|
64
|
+
async review(execution, context = {}) {
|
|
65
|
+
const agentId = context?.agentId || context?.agent?.id || execution?.agentId || "agent-main";
|
|
66
|
+
const reviewerId = context?.reviewerId || this.reviewerId;
|
|
67
|
+
|
|
68
|
+
// Enforce Separation of Duties (#62)
|
|
69
|
+
const sep = assertSeparationOfDuties(agentId, reviewerId);
|
|
70
|
+
if (!sep.valid) {
|
|
71
|
+
return {
|
|
72
|
+
approved: false,
|
|
73
|
+
verdict: "deny",
|
|
74
|
+
reason: sep.error,
|
|
75
|
+
reviewerId,
|
|
76
|
+
separationOfDuties: false,
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If no external model client is configured, return safe fallback
|
|
82
|
+
if (!this.reviewerClient) {
|
|
83
|
+
return {
|
|
84
|
+
approved: false,
|
|
85
|
+
verdict: this.fallbackVerdict,
|
|
86
|
+
reason: "no-independent-reviewer-model-configured",
|
|
87
|
+
reviewerId,
|
|
88
|
+
separationOfDuties: true,
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const payload = {
|
|
95
|
+
tool: execution?.name,
|
|
96
|
+
arguments: execution?.arguments,
|
|
97
|
+
dossier: context?.dossier,
|
|
98
|
+
track: context?.track,
|
|
99
|
+
riskBucket: context?.riskBucket,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let response = null;
|
|
103
|
+
if (typeof this.reviewerClient.classify === "function") {
|
|
104
|
+
response = await this.reviewerClient.classify(payload, {
|
|
105
|
+
systemPrompt: SAFETY_REVIEWER_SYSTEM_PROMPT,
|
|
106
|
+
});
|
|
107
|
+
} else if (typeof this.reviewerClient === "function") {
|
|
108
|
+
response = await this.reviewerClient(payload);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const verdict = response?.verdict === "allow" ? "allow" : (response?.verdict === "deny" ? "deny" : "ask");
|
|
112
|
+
const approved = verdict === "allow";
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
approved,
|
|
116
|
+
verdict,
|
|
117
|
+
reason: response?.reason || "independent-reviewer-verdict",
|
|
118
|
+
reviewerId,
|
|
119
|
+
separationOfDuties: true,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Reviewer errors fail closed
|
|
124
|
+
return {
|
|
125
|
+
approved: false,
|
|
126
|
+
verdict: "ask",
|
|
127
|
+
reason: `independent-reviewer-error: ${err?.message || err}`,
|
|
128
|
+
reviewerId,
|
|
129
|
+
separationOfDuties: true,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|