fidelis 0.1.0 → 0.1.2
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/auditor.d.ts +2 -0
- package/dist/auditor.js +46 -9
- package/dist/compiler.js +6 -3
- package/dist/judge.d.ts +4 -0
- package/dist/judge.js +51 -25
- package/dist/redact.js +1 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.js +7 -0
- package/package.json +22 -6
package/dist/auditor.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export declare class Auditor {
|
|
|
32
32
|
}): boolean;
|
|
33
33
|
/** A turn that failed or was interrupted has no completed boundary to audit: nothing is running any more. */
|
|
34
34
|
executionEnded(sessionID: string): void;
|
|
35
|
+
/** The agent never received the correction, so nothing is being corrected. */
|
|
36
|
+
deliveryFailed(sessionID: string, reason: string): void;
|
|
35
37
|
/** Merge host events with the ledger so compaction cannot erase evidence the audit still needs. */
|
|
36
38
|
retain(sessionID: string, events: ExecEvent[]): ExecEvent[];
|
|
37
39
|
complete(sessionID: string, input: CompletionInput): Promise<Outcome>;
|
package/dist/auditor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { consentProblem } from "./config.js";
|
|
3
3
|
import { getChecklist, resolveSources } from "./compiler.js";
|
|
4
|
-
import { judge } from "./judge.js";
|
|
4
|
+
import { judge, LEDGER_MARK } from "./judge.js";
|
|
5
5
|
import { transition } from "./machine.js";
|
|
6
6
|
import { redact } from "./redact.js";
|
|
7
7
|
import { canonical, sha256 } from "./store.js";
|
|
@@ -69,6 +69,15 @@ export class Auditor {
|
|
|
69
69
|
state.state = "READY";
|
|
70
70
|
}, "ended");
|
|
71
71
|
}
|
|
72
|
+
/** The agent never received the correction, so nothing is being corrected. */
|
|
73
|
+
deliveryFailed(sessionID, reason) {
|
|
74
|
+
this.save(sessionID, (state) => {
|
|
75
|
+
if (state.state !== "CORRECTING")
|
|
76
|
+
return;
|
|
77
|
+
state.state = "AUDIT_INCOMPLETE";
|
|
78
|
+
state.reason = `The correction could not be delivered: ${reason}`;
|
|
79
|
+
}, "incomplete");
|
|
80
|
+
}
|
|
72
81
|
/** Merge host events with the ledger so compaction cannot erase evidence the audit still needs. */
|
|
73
82
|
retain(sessionID, events) {
|
|
74
83
|
const store = this.o.store;
|
|
@@ -77,8 +86,13 @@ export class Auditor {
|
|
|
77
86
|
for (const event of events)
|
|
78
87
|
merged.set(event.id, event);
|
|
79
88
|
const ordered = [...merged.values()].sort((a, b) => (a.at ?? 0) - (b.at ?? 0) || a.seq - b.seq).map((event, seq) => ({ ...event, seq }));
|
|
80
|
-
const clipField = (value) =>
|
|
81
|
-
|
|
89
|
+
const clipField = (value) => {
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const clean = redact(value);
|
|
93
|
+
return raw || clean.length <= LEDGER_CLIP ? clean : clean.slice(0, LEDGER_CLIP) + LEDGER_MARK;
|
|
94
|
+
};
|
|
95
|
+
store.writeLedger(sessionID, ordered.slice(-LEDGER_MAX).map((event) => ({ ...event, text: clipField(event.text), output: clipField(event.output), input: clipField(typeof event.input === "string" || event.input === undefined ? event.input : JSON.stringify(event.input)) })));
|
|
82
96
|
return ordered;
|
|
83
97
|
}
|
|
84
98
|
async complete(sessionID, input) {
|
|
@@ -110,6 +124,11 @@ export class Auditor {
|
|
|
110
124
|
}, "auditing");
|
|
111
125
|
if (skip)
|
|
112
126
|
return skip;
|
|
127
|
+
// Spend is saved as it happens, so a superseded audit still counts against the session.
|
|
128
|
+
const charge = (costUSD) => {
|
|
129
|
+
if (costUSD)
|
|
130
|
+
this.save(sessionID, (state) => void (state.spentUSD += costUSD), "usage");
|
|
131
|
+
};
|
|
113
132
|
const metrics = { cacheHit: false, compilerCalls: 0, judgeCalls: 0, tokens: 0, costUSD: 0, latencyMs: 0, uncertain: 0 };
|
|
114
133
|
const finish = (event, reason, findings, extra = {}) => {
|
|
115
134
|
metrics.latencyMs = Date.now() - started;
|
|
@@ -127,7 +146,6 @@ export class Auditor {
|
|
|
127
146
|
state.findings = findings;
|
|
128
147
|
state.lastAuditID = auditID;
|
|
129
148
|
state.boundaries = [...state.boundaries, boundaryID].slice(-MAX_TRACKED);
|
|
130
|
-
state.spentUSD += metrics.costUSD;
|
|
131
149
|
}, event);
|
|
132
150
|
if (!current)
|
|
133
151
|
return null;
|
|
@@ -167,6 +185,7 @@ export class Auditor {
|
|
|
167
185
|
metrics.compilerCalls = compiled.calls;
|
|
168
186
|
metrics.tokens = compiled.tokens;
|
|
169
187
|
metrics.costUSD = compiled.costUSD;
|
|
188
|
+
charge(compiled.costUSD);
|
|
170
189
|
if (!compiled.checklist)
|
|
171
190
|
return incomplete(compileConsent && !compiled.cacheHit ? compileConsent : (compiled.reason ?? "Checklist unavailable."));
|
|
172
191
|
this.save(sessionID, (state) => void (state.phase = "judging"), "judging");
|
|
@@ -177,18 +196,33 @@ export class Auditor {
|
|
|
177
196
|
return incomplete("No decision model is available.");
|
|
178
197
|
let verdict;
|
|
179
198
|
try {
|
|
180
|
-
verdict = await judge({
|
|
199
|
+
verdict = await judge({
|
|
200
|
+
checklist: compiled.checklist,
|
|
201
|
+
evidence: input.evidence,
|
|
202
|
+
prior: begin.findings.filter((finding) => finding.status === "VIOLATED"),
|
|
203
|
+
model: this.o.judge,
|
|
204
|
+
config,
|
|
205
|
+
stopReason: () => {
|
|
206
|
+
const current = store.read(sessionID);
|
|
207
|
+
if (current.audit?.id !== auditID)
|
|
208
|
+
return "the audit was superseded by a newer turn";
|
|
209
|
+
return config.maxAuditCostUSD !== null && current.spentUSD >= config.maxAuditCostUSD ? "the audit budget is exhausted" : null;
|
|
210
|
+
},
|
|
211
|
+
onUsage: (tokens, costUSD) => {
|
|
212
|
+
metrics.tokens += tokens;
|
|
213
|
+
metrics.costUSD += costUSD;
|
|
214
|
+
charge(costUSD);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
181
217
|
}
|
|
182
218
|
catch (error) {
|
|
183
219
|
return incomplete(`Judge failed: ${error.message}`);
|
|
184
220
|
}
|
|
185
221
|
metrics.judgeCalls = verdict.calls;
|
|
186
|
-
|
|
187
|
-
metrics.costUSD += verdict.costUSD;
|
|
188
|
-
return this.route(verdict.findings, verdict.waiting, begin, auditID, finish, stale, incomplete);
|
|
222
|
+
return this.route(verdict.findings, verdict.waiting, compiled.checklist.dropped.length, begin, auditID, finish, stale, incomplete);
|
|
189
223
|
}
|
|
190
224
|
/** Deterministic routing. Model output only suggests; these branches decide. */
|
|
191
|
-
route(findings, waiting, begin, auditID, finish, stale, incomplete) {
|
|
225
|
+
route(findings, waiting, dropped, begin, auditID, finish, stale, incomplete) {
|
|
192
226
|
const violated = findings.filter((finding) => finding.status === "VIOLATED");
|
|
193
227
|
const open = violated.filter((finding) => !finding.resolved);
|
|
194
228
|
const forUser = open.filter((finding) => finding.repair === "user");
|
|
@@ -219,6 +253,9 @@ export class Auditor {
|
|
|
219
253
|
const unknown = findings.filter((finding) => finding.status === "UNKNOWN");
|
|
220
254
|
if (unknown.length)
|
|
221
255
|
return incomplete(`${unknown.length} rule(s) could not be assessed.`, findings);
|
|
256
|
+
// A rule the compiler produced but validation rejected was never judged, so this cannot be a clean pass.
|
|
257
|
+
if (dropped)
|
|
258
|
+
return incomplete(`${dropped} compiled rule(s) failed validation and were not assessed.`, findings);
|
|
222
259
|
const reason = violated.length ? `${violated.length} violation(s) on record, repaired or acknowledged after correction.` : undefined;
|
|
223
260
|
const state = finish("clean", reason, findings);
|
|
224
261
|
return state ? { action: "none", state, reason, notice: violated.length ? `Fidelis findings:\n${userNotice(violated)}` : undefined } : stale();
|
package/dist/compiler.js
CHANGED
|
@@ -107,7 +107,7 @@ export async function getChecklist(input) {
|
|
|
107
107
|
const fingerprint = checklistFingerprint(host, sources, config, prompt);
|
|
108
108
|
const cached = config.cacheChecklist ? store.getChecklist(fingerprint) : undefined;
|
|
109
109
|
const miss = { cacheHit: false, calls: 1, tokens: 0, costUSD: 0 };
|
|
110
|
-
if (cached?.schema === SCHEMA_VERSION)
|
|
110
|
+
if (cached?.schema === SCHEMA_VERSION && cached.dropped.length === 0)
|
|
111
111
|
return { checklist: cached, cacheHit: true, calls: 0, tokens: 0, costUSD: 0 };
|
|
112
112
|
if (!generate)
|
|
113
113
|
return { ...miss, calls: 0, reason: "No compiler model is available." };
|
|
@@ -118,14 +118,17 @@ export async function getChecklist(input) {
|
|
|
118
118
|
miss.tokens = output.tokens ?? 0;
|
|
119
119
|
miss.costUSD = output.costUSD ?? 0;
|
|
120
120
|
const raw = parseJson(output.text);
|
|
121
|
-
const confidence =
|
|
121
|
+
const confidence = raw?.confidence;
|
|
122
|
+
if (typeof confidence !== "number" || !(confidence >= 0 && confidence <= 1))
|
|
123
|
+
return { ...miss, reason: "Compiler output has no valid confidence." };
|
|
122
124
|
const { rules, dropped } = validateRules(raw, sources);
|
|
123
125
|
if (confidence < MIN_CONFIDENCE)
|
|
124
126
|
return { ...miss, reason: `Compiler confidence ${confidence} is too low.` };
|
|
125
127
|
if (rules.length === 0)
|
|
126
128
|
return { ...miss, reason: `Compiler produced no valid rules from the instructions (${dropped.length} rejected).` };
|
|
127
129
|
const checklist = { schema: SCHEMA_VERSION, fingerprint, model: config.compilerModel, compiledAt: Date.now(), rules, dropped };
|
|
128
|
-
|
|
130
|
+
// Only a fully validated checklist is reused; a partial one is retried on the next turn.
|
|
131
|
+
if (config.cacheChecklist && dropped.length === 0)
|
|
129
132
|
store.putChecklist(checklist);
|
|
130
133
|
return { ...miss, checklist };
|
|
131
134
|
}
|
package/dist/judge.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Checklist, Config, Evidence, Finding } from "./types.ts";
|
|
2
2
|
import { type DecisionModel } from "./providers.ts";
|
|
3
|
+
/** Marks text the ledger cut. An event read back from it can no longer prove what it does not show. */
|
|
4
|
+
export declare const LEDGER_MARK = "\u2026[clipped in ledger]";
|
|
3
5
|
export declare class EvidenceTooLarge extends Error {
|
|
4
6
|
}
|
|
5
7
|
/**
|
|
@@ -25,4 +27,6 @@ export declare function judge(input: {
|
|
|
25
27
|
prior: Finding[];
|
|
26
28
|
model: DecisionModel;
|
|
27
29
|
config: Config;
|
|
30
|
+
stopReason?: () => string | null;
|
|
31
|
+
onUsage?: (tokens: number, costUSD: number) => void;
|
|
28
32
|
}): Promise<JudgeResult>;
|
package/dist/judge.js
CHANGED
|
@@ -8,11 +8,16 @@ const MIN_SATISFIED = 0.6;
|
|
|
8
8
|
const CLIP_STEPS = [2000, 500, 120];
|
|
9
9
|
const HISTORY_CLIP = 120;
|
|
10
10
|
const HISTORY_SHARE = 0.3;
|
|
11
|
+
/** Marks text the ledger cut. An event read back from it can no longer prove what it does not show. */
|
|
12
|
+
export const LEDGER_MARK = "…[clipped in ledger]";
|
|
11
13
|
export class EvidenceTooLarge extends Error {
|
|
12
14
|
}
|
|
15
|
+
const ledgerClipped = (event) => [event.text, event.output, event.input].some((value) => typeof value === "string" && value.includes(LEDGER_MARK));
|
|
13
16
|
function clip(text, limit) {
|
|
14
17
|
return text.length <= limit ? text : `${text.slice(0, limit)}…[clipped ${text.length - limit} chars]`;
|
|
15
18
|
}
|
|
19
|
+
/** Redact whole values before clipping: a cut secret no longer matches its pattern. */
|
|
20
|
+
const safeClip = (text, limit) => clip(redact(text), limit);
|
|
16
21
|
function eventLine(event, limit) {
|
|
17
22
|
const parts = [`#${event.seq} id=${event.id} ${event.kind}`];
|
|
18
23
|
if (event.actor)
|
|
@@ -22,12 +27,12 @@ function eventLine(event, limit) {
|
|
|
22
27
|
if (event.status)
|
|
23
28
|
parts.push(`status=${event.status}`);
|
|
24
29
|
if (event.input !== undefined)
|
|
25
|
-
parts.push(`input=${
|
|
30
|
+
parts.push(`input=${safeClip(typeof event.input === "string" ? event.input : JSON.stringify(event.input), limit)}`);
|
|
26
31
|
if (event.output)
|
|
27
|
-
parts.push(`output=${
|
|
32
|
+
parts.push(`output=${safeClip(event.output, limit)}`);
|
|
28
33
|
if (event.text)
|
|
29
|
-
parts.push(`text=${
|
|
30
|
-
return
|
|
34
|
+
parts.push(`text=${safeClip(event.text, limit)}`);
|
|
35
|
+
return parts.join(" ");
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
38
|
* The turn is clipped loudly and never loses an event: over budget means an incomplete audit. Older context may be
|
|
@@ -37,27 +42,34 @@ export function digest(evidence, prior, budgetChars, rawIDs = new Set(), preambl
|
|
|
37
42
|
const history = evidence.history ?? [];
|
|
38
43
|
const historyLines = [];
|
|
39
44
|
let used = 0;
|
|
45
|
+
let full = false;
|
|
40
46
|
for (const event of [...history].reverse()) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
used
|
|
47
|
+
// A restored span always goes in: the recheck is only valid if the judge really sees it. Over budget fails below.
|
|
48
|
+
const restored = rawIDs.has(event.id);
|
|
49
|
+
const line = eventLine(event, restored ? budgetChars : HISTORY_CLIP);
|
|
50
|
+
if (!restored && (full || used + line.length > budgetChars * HISTORY_SHARE)) {
|
|
51
|
+
full = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!restored)
|
|
55
|
+
used += line.length;
|
|
45
56
|
historyLines.unshift(line);
|
|
46
57
|
}
|
|
47
58
|
const omitted = history.length - historyLines.length;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
59
|
+
const clippedInLedger = [...history, ...evidence.events].some(ledgerClipped);
|
|
60
|
+
const complete = evidence.complete && omitted === 0 && !clippedInLedger;
|
|
61
|
+
const gaps = [...evidence.gaps, ...(omitted ? [`${omitted} older events omitted to fit the budget`] : []), ...(clippedInLedger ? ["some retained events were clipped after the host compacted them"] : [])];
|
|
50
62
|
const head = [
|
|
51
63
|
preamble,
|
|
52
64
|
`Evidence source: ${complete ? "known complete" : `INCOMPLETE (${gaps.join("; ") || "unknown gaps"})`}.`,
|
|
53
|
-
`Current user request: ${
|
|
65
|
+
`Current user request: ${safeClip(evidence.userRequest, 4000)}`,
|
|
54
66
|
prior.length ? `Unresolved findings from the previous audit of this turn: ${prior.map((finding) => `${finding.ruleID}: ${clip(finding.quote, 200)}`).join(" | ")}` : "",
|
|
55
67
|
historyLines.length ? `Earlier turns, context only${omitted ? ` (${omitted} older events omitted)` : ""}:\n${historyLines.join("\n")}` : "",
|
|
56
68
|
"Events of the turn under audit:",
|
|
57
69
|
].filter(Boolean);
|
|
58
70
|
for (const limit of CLIP_STEPS) {
|
|
59
71
|
const lines = evidence.events.map((event) => eventLine(event, rawIDs.has(event.id) ? budgetChars : limit));
|
|
60
|
-
const text = [...head, ...lines, `Final reply: ${redact(
|
|
72
|
+
const text = [...head, ...lines, `Final reply: ${redact(evidence.finalReply)}`].join("\n");
|
|
61
73
|
if (text.length <= budgetChars)
|
|
62
74
|
return { text, clipped: limit !== CLIP_STEPS[0] || text.includes("…[clipped"), complete };
|
|
63
75
|
}
|
|
@@ -78,11 +90,15 @@ export async function judge(input) {
|
|
|
78
90
|
const ask = async (state, questions) => {
|
|
79
91
|
const answers = {};
|
|
80
92
|
for (let index = 0; index < questions.length; index += CHUNK) {
|
|
93
|
+
const stop = input.stopReason?.();
|
|
94
|
+
if (stop)
|
|
95
|
+
throw new Error(stop);
|
|
81
96
|
const decisions = await withRetry(() => model.ask(state, questions.slice(index, index + CHUNK)), config);
|
|
82
97
|
Object.assign(answers, decisions.answers);
|
|
83
98
|
result.calls += 1;
|
|
84
99
|
result.tokens += decisions.tokens ?? 0;
|
|
85
100
|
result.costUSD += decisions.costUSD ?? 0;
|
|
101
|
+
input.onUsage?.(decisions.tokens ?? 0, decisions.costUSD ?? 0);
|
|
86
102
|
}
|
|
87
103
|
return answers;
|
|
88
104
|
};
|
|
@@ -111,7 +127,10 @@ export async function judge(input) {
|
|
|
111
127
|
{ type: "predicate", name: "turn_waiting", instructions: QUESTIONS.waiting },
|
|
112
128
|
...open.flatMap((rule, i) => [predicate(`r${i}_applies`, rule, QUESTIONS.applies), predicate(`r${i}_deadline`, rule, QUESTIONS.deadline)]),
|
|
113
129
|
]);
|
|
114
|
-
|
|
130
|
+
const waiting = scope.turn_waiting?.p;
|
|
131
|
+
if (typeof waiting !== "number")
|
|
132
|
+
throw new Error("the model did not answer whether the agent is waiting for the user");
|
|
133
|
+
result.waiting = waiting >= 0.5;
|
|
115
134
|
const applying = new Set(open.filter((_, i) => (scope[`r${i}_applies`]?.p ?? 1) >= 0.5).map((rule) => rule.id));
|
|
116
135
|
const candidates = [];
|
|
117
136
|
open.forEach((rule, i) => {
|
|
@@ -121,7 +140,7 @@ export async function judge(input) {
|
|
|
121
140
|
return void done.set(rule.id, finding(rule, "UNKNOWN", "The decision model gave no answer.", { uncertainty: scope[`r${i}_applies`]?.error ?? scope[`r${i}_deadline`]?.error ?? "missing answer" }));
|
|
122
141
|
if (applies < 0.5)
|
|
123
142
|
return void done.set(rule.id, finding(rule, "NOT_APPLICABLE", "Does not govern this turn.", { probability: applies }));
|
|
124
|
-
if (rule.supersededBy && applying.has(rule.supersededBy))
|
|
143
|
+
if (rule.supersedable && rule.supersededBy && applying.has(rule.supersededBy))
|
|
125
144
|
return void done.set(rule.id, finding(rule, "NOT_APPLICABLE", `Superseded by applicable rule ${rule.supersededBy}.`));
|
|
126
145
|
if (deadline < 0.5)
|
|
127
146
|
return void done.set(rule.id, finding(rule, "PENDING", "Its deadline has not occurred at this boundary.", { probability: deadline }));
|
|
@@ -140,23 +159,29 @@ export async function judge(input) {
|
|
|
140
159
|
instructions: `${ruleText(rule)}\nQuestion: ${text}`,
|
|
141
160
|
choices: events.map((event) => ({ value: event.id, description: clip(eventLine(event, 160), 240) })),
|
|
142
161
|
});
|
|
143
|
-
//
|
|
144
|
-
|
|
162
|
+
// A verdict resting on clipped evidence gets one recheck with the disputed span in full:
|
|
163
|
+
// ambiguous ones, and any breach that rests on absence, since the proof may sit in the clipped part.
|
|
164
|
+
const disputed = candidates.filter(({ i }) => {
|
|
145
165
|
const p = verdict[`r${i}_violated`]?.p ?? 0;
|
|
146
|
-
return p >= 0.5 && p < config.minViolationConfidence;
|
|
166
|
+
return p >= 0.5 && (p < config.minViolationConfidence || (verdict[`r${i}_absence`]?.p ?? 1) >= 0.5);
|
|
147
167
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
168
|
+
const rechecked = new Set();
|
|
169
|
+
if (first.clipped && disputed.length && inTurn.length) {
|
|
170
|
+
const spans = await ask(first.text, disputed.map(({ rule, i }) => cite(`r${i}_span`, rule, "Which event's full content most directly bears on whether this rule was breached?", everywhere)));
|
|
171
|
+
const restored = disputed.filter(({ i }) => knownAnywhere.has(spans[`r${i}_span`]?.choice ?? ""));
|
|
172
|
+
const raw = new Set(restored.map(({ i }) => spans[`r${i}_span`].choice));
|
|
151
173
|
try {
|
|
152
|
-
const again = await ask(digest(evidence, prior, budget * 2, raw, preamble).text,
|
|
174
|
+
const again = await ask(digest(evidence, prior, budget * 2, raw, preamble).text, restored.map(({ rule, i }) => predicate(`r${i}_violated`, rule, QUESTIONS.violated)));
|
|
153
175
|
Object.assign(verdict, again);
|
|
176
|
+
for (const { i } of restored)
|
|
177
|
+
rechecked.add(i);
|
|
154
178
|
}
|
|
155
179
|
catch (error) {
|
|
156
180
|
if (!(error instanceof EvidenceTooLarge))
|
|
157
181
|
throw error;
|
|
158
182
|
}
|
|
159
183
|
}
|
|
184
|
+
const reported = new Set(prior.map((finding) => finding.ruleID));
|
|
160
185
|
const breached = [];
|
|
161
186
|
const complied = [];
|
|
162
187
|
for (const candidate of candidates) {
|
|
@@ -165,9 +190,11 @@ export async function judge(input) {
|
|
|
165
190
|
const satisfied = verdict[`r${i}_satisfied`]?.p ?? 0;
|
|
166
191
|
if (violated === undefined)
|
|
167
192
|
done.set(rule.id, finding(rule, "UNKNOWN", "The decision model gave no answer.", { uncertainty: verdict[`r${i}_violated`]?.error ?? "missing answer" }));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
193
|
+
// A breach an earlier audit of this turn already proved stays on the repair path even if this verdict is less sure.
|
|
194
|
+
else if (violated >= config.minViolationConfidence || (violated >= 0.5 && reported.has(rule.id))) {
|
|
195
|
+
const unprovable = !first.complete || (first.clipped && !rechecked.has(i));
|
|
196
|
+
if ((verdict[`r${i}_absence`]?.p ?? 1) >= 0.5 && unprovable)
|
|
197
|
+
done.set(rule.id, finding(rule, "UNKNOWN", "The breach rests only on missing events and the evidence is incomplete.", { probability: violated, uncertainty: "incomplete evidence" }));
|
|
171
198
|
else
|
|
172
199
|
breached.push(candidate);
|
|
173
200
|
}
|
|
@@ -179,7 +206,6 @@ export async function judge(input) {
|
|
|
179
206
|
done.set(rule.id, finding(rule, "UNKNOWN", "Neither compliance nor a breach is supported by the evidence.", { probability: satisfied, uncertainty: "insufficient evidence" }));
|
|
180
207
|
}
|
|
181
208
|
// Stage 3: evidence and repair routing, asked only for what needs them.
|
|
182
|
-
const reported = new Set(prior.map((finding) => finding.ruleID));
|
|
183
209
|
const detail = everywhere.length
|
|
184
210
|
? await ask(first.text, [
|
|
185
211
|
...breached.flatMap(({ rule, i }) => [
|
package/dist/redact.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const PATTERNS = [
|
|
2
|
-
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]
|
|
2
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]"],
|
|
3
3
|
[/\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|npm_[A-Za-z0-9]{20,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,})\b/g, "[REDACTED TOKEN]"],
|
|
4
4
|
[/\b(authorization\s*[:=]\s*(?:bearer\s+)?)[^\s"']+/gi, "$1[REDACTED]"],
|
|
5
5
|
[/\b(bearer\s+)[A-Za-z0-9._~+/-]{16,}=*/gi, "$1[REDACTED]"],
|
package/dist/store.d.ts
CHANGED
|
@@ -20,5 +20,8 @@ export declare class Store {
|
|
|
20
20
|
/** Evidence that must survive host compaction. Holds redacted, clipped events by default. */
|
|
21
21
|
readLedger(sessionID: string): ExecEvent[];
|
|
22
22
|
writeLedger(sessionID: string, events: ExecEvent[]): void;
|
|
23
|
+
/** Lazily loaded instruction files, which the host may not show again after compaction. */
|
|
24
|
+
readNested(sessionID: string): Record<string, string>;
|
|
25
|
+
writeNested(sessionID: string, nested: Record<string, string>): void;
|
|
23
26
|
cleanup(retentionDays: number, now?: number): void;
|
|
24
27
|
}
|
package/dist/store.js
CHANGED
|
@@ -142,6 +142,13 @@ export class Store {
|
|
|
142
142
|
writeLedger(sessionID, events) {
|
|
143
143
|
writeAtomic(join(this.sessionDir(sessionID), "ledger.json"), JSON.stringify(events));
|
|
144
144
|
}
|
|
145
|
+
/** Lazily loaded instruction files, which the host may not show again after compaction. */
|
|
146
|
+
readNested(sessionID) {
|
|
147
|
+
return readJson(join(this.sessionDir(sessionID), "nested.json")) ?? {};
|
|
148
|
+
}
|
|
149
|
+
writeNested(sessionID, nested) {
|
|
150
|
+
writeAtomic(join(this.sessionDir(sessionID), "nested.json"), JSON.stringify(nested));
|
|
151
|
+
}
|
|
145
152
|
cleanup(retentionDays, now = Date.now()) {
|
|
146
153
|
const cutoff = now - retentionDays * 86_400_000;
|
|
147
154
|
const prune = (dir) => {
|
package/package.json
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fidelis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Harness-agnostic AI agent auditor core: checklist compiler, judge pipeline and session state machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/insign/fidelis.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.18"
|
|
27
|
+
}
|
|
12
28
|
}
|