fidelis 0.1.0 → 0.1.1
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 +42 -22
- 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
|
|
@@ -38,26 +43,27 @@ export function digest(evidence, prior, budgetChars, rawIDs = new Set(), preambl
|
|
|
38
43
|
const historyLines = [];
|
|
39
44
|
let used = 0;
|
|
40
45
|
for (const event of [...history].reverse()) {
|
|
41
|
-
const line = eventLine(event, HISTORY_CLIP);
|
|
46
|
+
const line = eventLine(event, rawIDs.has(event.id) ? budgetChars : HISTORY_CLIP);
|
|
42
47
|
if (used + line.length > budgetChars * HISTORY_SHARE)
|
|
43
48
|
break;
|
|
44
49
|
used += line.length;
|
|
45
50
|
historyLines.unshift(line);
|
|
46
51
|
}
|
|
47
52
|
const omitted = history.length - historyLines.length;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
53
|
+
const clippedInLedger = [...history, ...evidence.events].some(ledgerClipped);
|
|
54
|
+
const complete = evidence.complete && omitted === 0 && !clippedInLedger;
|
|
55
|
+
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
56
|
const head = [
|
|
51
57
|
preamble,
|
|
52
58
|
`Evidence source: ${complete ? "known complete" : `INCOMPLETE (${gaps.join("; ") || "unknown gaps"})`}.`,
|
|
53
|
-
`Current user request: ${
|
|
59
|
+
`Current user request: ${safeClip(evidence.userRequest, 4000)}`,
|
|
54
60
|
prior.length ? `Unresolved findings from the previous audit of this turn: ${prior.map((finding) => `${finding.ruleID}: ${clip(finding.quote, 200)}`).join(" | ")}` : "",
|
|
55
61
|
historyLines.length ? `Earlier turns, context only${omitted ? ` (${omitted} older events omitted)` : ""}:\n${historyLines.join("\n")}` : "",
|
|
56
62
|
"Events of the turn under audit:",
|
|
57
63
|
].filter(Boolean);
|
|
58
64
|
for (const limit of CLIP_STEPS) {
|
|
59
65
|
const lines = evidence.events.map((event) => eventLine(event, rawIDs.has(event.id) ? budgetChars : limit));
|
|
60
|
-
const text = [...head, ...lines, `Final reply: ${redact(
|
|
66
|
+
const text = [...head, ...lines, `Final reply: ${redact(evidence.finalReply)}`].join("\n");
|
|
61
67
|
if (text.length <= budgetChars)
|
|
62
68
|
return { text, clipped: limit !== CLIP_STEPS[0] || text.includes("…[clipped"), complete };
|
|
63
69
|
}
|
|
@@ -78,11 +84,15 @@ export async function judge(input) {
|
|
|
78
84
|
const ask = async (state, questions) => {
|
|
79
85
|
const answers = {};
|
|
80
86
|
for (let index = 0; index < questions.length; index += CHUNK) {
|
|
87
|
+
const stop = input.stopReason?.();
|
|
88
|
+
if (stop)
|
|
89
|
+
throw new Error(stop);
|
|
81
90
|
const decisions = await withRetry(() => model.ask(state, questions.slice(index, index + CHUNK)), config);
|
|
82
91
|
Object.assign(answers, decisions.answers);
|
|
83
92
|
result.calls += 1;
|
|
84
93
|
result.tokens += decisions.tokens ?? 0;
|
|
85
94
|
result.costUSD += decisions.costUSD ?? 0;
|
|
95
|
+
input.onUsage?.(decisions.tokens ?? 0, decisions.costUSD ?? 0);
|
|
86
96
|
}
|
|
87
97
|
return answers;
|
|
88
98
|
};
|
|
@@ -111,7 +121,10 @@ export async function judge(input) {
|
|
|
111
121
|
{ type: "predicate", name: "turn_waiting", instructions: QUESTIONS.waiting },
|
|
112
122
|
...open.flatMap((rule, i) => [predicate(`r${i}_applies`, rule, QUESTIONS.applies), predicate(`r${i}_deadline`, rule, QUESTIONS.deadline)]),
|
|
113
123
|
]);
|
|
114
|
-
|
|
124
|
+
const waiting = scope.turn_waiting?.p;
|
|
125
|
+
if (typeof waiting !== "number")
|
|
126
|
+
throw new Error("the model did not answer whether the agent is waiting for the user");
|
|
127
|
+
result.waiting = waiting >= 0.5;
|
|
115
128
|
const applying = new Set(open.filter((_, i) => (scope[`r${i}_applies`]?.p ?? 1) >= 0.5).map((rule) => rule.id));
|
|
116
129
|
const candidates = [];
|
|
117
130
|
open.forEach((rule, i) => {
|
|
@@ -121,7 +134,7 @@ export async function judge(input) {
|
|
|
121
134
|
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
135
|
if (applies < 0.5)
|
|
123
136
|
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))
|
|
137
|
+
if (rule.supersedable && rule.supersededBy && applying.has(rule.supersededBy))
|
|
125
138
|
return void done.set(rule.id, finding(rule, "NOT_APPLICABLE", `Superseded by applicable rule ${rule.supersededBy}.`));
|
|
126
139
|
if (deadline < 0.5)
|
|
127
140
|
return void done.set(rule.id, finding(rule, "PENDING", "Its deadline has not occurred at this boundary.", { probability: deadline }));
|
|
@@ -140,23 +153,29 @@ export async function judge(input) {
|
|
|
140
153
|
instructions: `${ruleText(rule)}\nQuestion: ${text}`,
|
|
141
154
|
choices: events.map((event) => ({ value: event.id, description: clip(eventLine(event, 160), 240) })),
|
|
142
155
|
});
|
|
143
|
-
//
|
|
144
|
-
|
|
156
|
+
// A verdict resting on clipped evidence gets one recheck with the disputed span in full:
|
|
157
|
+
// ambiguous ones, and any breach that rests on absence, since the proof may sit in the clipped part.
|
|
158
|
+
const disputed = candidates.filter(({ i }) => {
|
|
145
159
|
const p = verdict[`r${i}_violated`]?.p ?? 0;
|
|
146
|
-
return p >= 0.5 && p < config.minViolationConfidence;
|
|
160
|
+
return p >= 0.5 && (p < config.minViolationConfidence || (verdict[`r${i}_absence`]?.p ?? 1) >= 0.5);
|
|
147
161
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
162
|
+
const rechecked = new Set();
|
|
163
|
+
if (first.clipped && disputed.length && inTurn.length) {
|
|
164
|
+
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)));
|
|
165
|
+
const restored = disputed.filter(({ i }) => knownAnywhere.has(spans[`r${i}_span`]?.choice ?? ""));
|
|
166
|
+
const raw = new Set(restored.map(({ i }) => spans[`r${i}_span`].choice));
|
|
151
167
|
try {
|
|
152
|
-
const again = await ask(digest(evidence, prior, budget * 2, raw, preamble).text,
|
|
168
|
+
const again = await ask(digest(evidence, prior, budget * 2, raw, preamble).text, restored.map(({ rule, i }) => predicate(`r${i}_violated`, rule, QUESTIONS.violated)));
|
|
153
169
|
Object.assign(verdict, again);
|
|
170
|
+
for (const { i } of restored)
|
|
171
|
+
rechecked.add(i);
|
|
154
172
|
}
|
|
155
173
|
catch (error) {
|
|
156
174
|
if (!(error instanceof EvidenceTooLarge))
|
|
157
175
|
throw error;
|
|
158
176
|
}
|
|
159
177
|
}
|
|
178
|
+
const reported = new Set(prior.map((finding) => finding.ruleID));
|
|
160
179
|
const breached = [];
|
|
161
180
|
const complied = [];
|
|
162
181
|
for (const candidate of candidates) {
|
|
@@ -165,9 +184,11 @@ export async function judge(input) {
|
|
|
165
184
|
const satisfied = verdict[`r${i}_satisfied`]?.p ?? 0;
|
|
166
185
|
if (violated === undefined)
|
|
167
186
|
done.set(rule.id, finding(rule, "UNKNOWN", "The decision model gave no answer.", { uncertainty: verdict[`r${i}_violated`]?.error ?? "missing answer" }));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
187
|
+
// A breach an earlier audit of this turn already proved stays on the repair path even if this verdict is less sure.
|
|
188
|
+
else if (violated >= config.minViolationConfidence || (violated >= 0.5 && reported.has(rule.id))) {
|
|
189
|
+
const unprovable = !first.complete || (first.clipped && !rechecked.has(i));
|
|
190
|
+
if ((verdict[`r${i}_absence`]?.p ?? 1) >= 0.5 && unprovable)
|
|
191
|
+
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
192
|
else
|
|
172
193
|
breached.push(candidate);
|
|
173
194
|
}
|
|
@@ -179,7 +200,6 @@ export async function judge(input) {
|
|
|
179
200
|
done.set(rule.id, finding(rule, "UNKNOWN", "Neither compliance nor a breach is supported by the evidence.", { probability: satisfied, uncertainty: "insufficient evidence" }));
|
|
180
201
|
}
|
|
181
202
|
// Stage 3: evidence and repair routing, asked only for what needs them.
|
|
182
|
-
const reported = new Set(prior.map((finding) => finding.ruleID));
|
|
183
203
|
const detail = everywhere.length
|
|
184
204
|
? await ask(first.text, [
|
|
185
205
|
...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.1",
|
|
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
|
}
|