fidelis 0.1.1 → 0.1.3

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.js CHANGED
@@ -158,68 +158,75 @@ export class Auditor {
158
158
  const state = finish("incomplete", reason, findings);
159
159
  return state ? { action: "none", state, reason, notice: `Fidelis audit incomplete: ${reason}` } : stale();
160
160
  };
161
- if (config.maxAuditCostUSD !== null && begin.spentUSD >= config.maxAuditCostUSD) {
162
- const reason = `Audit budget of $${config.maxAuditCostUSD} is exhausted.`;
163
- const state = finish("stop", reason, []);
164
- return state ? { action: "none", state, reason, notice: `Fidelis stopped: ${reason}` } : stale();
165
- }
166
- if (!input.sources)
167
- return incomplete("The host did not reveal which instructions it loaded.");
168
- const sources = resolveSources(input.sources);
169
- if (sources.length === 0) {
170
- const reason = "No effective instructions to audit.";
171
- const state = finish("clean", reason, []);
172
- return state ? { action: "none", state, reason } : stale();
173
- }
174
- const options = this.o.options ?? {};
175
- const compileConsent = consentProblem(config.compilerModel, options, config);
176
- const compiled = await getChecklist({
177
- store,
178
- host: this.o.host,
179
- sources,
180
- config,
181
- generate: compileConsent ? null : this.o.compiler,
182
- onCompile: () => this.save(sessionID, (state) => void (state.phase = "compiling"), "compiling"),
183
- });
184
- metrics.cacheHit = compiled.cacheHit;
185
- metrics.compilerCalls = compiled.calls;
186
- metrics.tokens = compiled.tokens;
187
- metrics.costUSD = compiled.costUSD;
188
- charge(compiled.costUSD);
189
- if (!compiled.checklist)
190
- return incomplete(compileConsent && !compiled.cacheHit ? compileConsent : (compiled.reason ?? "Checklist unavailable."));
191
- this.save(sessionID, (state) => void (state.phase = "judging"), "judging");
192
- const judgeConsent = consentProblem(config.judgeModel, options, config);
193
- if (judgeConsent)
194
- return incomplete(judgeConsent);
195
- if (!this.o.judge)
196
- return incomplete("No decision model is available.");
197
- let verdict;
161
+ // Past this point the session is AUDITING, so any failure must still end in a recorded outcome.
162
+ const bounded = { ...config, deadlineAt: started + config.auditDeadlineMs, onFailedCost: (costUSD) => { metrics.costUSD += costUSD; charge(costUSD); } };
198
163
  try {
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
- },
164
+ if (config.maxAuditCostUSD !== null && begin.spentUSD >= config.maxAuditCostUSD) {
165
+ const reason = `Audit budget of $${config.maxAuditCostUSD} is exhausted.`;
166
+ const state = finish("stop", reason, []);
167
+ return state ? { action: "none", state, reason, notice: `Fidelis stopped: ${reason}` } : stale();
168
+ }
169
+ if (!input.sources)
170
+ return incomplete("The host did not reveal which instructions it loaded.");
171
+ const sources = resolveSources(input.sources);
172
+ if (sources.length === 0) {
173
+ const reason = "No effective instructions to audit.";
174
+ const state = finish("clean", reason, []);
175
+ return state ? { action: "none", state, reason } : stale();
176
+ }
177
+ const options = this.o.options ?? {};
178
+ const compileConsent = consentProblem(config.compilerModel, options, config);
179
+ const compiled = await getChecklist({
180
+ store,
181
+ host: this.o.host,
182
+ sources,
183
+ config: bounded,
184
+ generate: compileConsent ? null : this.o.compiler,
185
+ onCompile: () => this.save(sessionID, (state) => void (state.phase = "compiling"), "compiling"),
216
186
  });
187
+ metrics.cacheHit = compiled.cacheHit;
188
+ metrics.compilerCalls = compiled.calls;
189
+ metrics.tokens = compiled.tokens;
190
+ metrics.costUSD = compiled.costUSD;
191
+ charge(compiled.costUSD);
192
+ if (!compiled.checklist)
193
+ return incomplete(compileConsent && !compiled.cacheHit ? compileConsent : (compiled.reason ?? "Checklist unavailable."));
194
+ this.save(sessionID, (state) => void (state.phase = "judging"), "judging");
195
+ const judgeConsent = consentProblem(config.judgeModel, options, config);
196
+ if (judgeConsent)
197
+ return incomplete(judgeConsent);
198
+ if (!this.o.judge)
199
+ return incomplete("No decision model is available.");
200
+ let verdict;
201
+ try {
202
+ verdict = await judge({
203
+ checklist: compiled.checklist,
204
+ evidence: input.evidence,
205
+ prior: begin.findings.filter((finding) => finding.status === "VIOLATED"),
206
+ model: this.o.judge,
207
+ config: bounded,
208
+ stopReason: () => {
209
+ const current = store.read(sessionID);
210
+ if (current.audit?.id !== auditID)
211
+ return "the audit was superseded by a newer turn";
212
+ return config.maxAuditCostUSD !== null && current.spentUSD >= config.maxAuditCostUSD ? "the audit budget is exhausted" : null;
213
+ },
214
+ onUsage: (tokens, costUSD) => {
215
+ metrics.tokens += tokens;
216
+ metrics.costUSD += costUSD;
217
+ charge(costUSD);
218
+ },
219
+ });
220
+ }
221
+ catch (error) {
222
+ return incomplete(`Judge failed: ${error.message}`);
223
+ }
224
+ metrics.judgeCalls = verdict.calls;
225
+ return this.route(verdict.findings, verdict.waiting, compiled.checklist.dropped.length, begin, auditID, finish, stale, incomplete);
217
226
  }
218
227
  catch (error) {
219
- return incomplete(`Judge failed: ${error.message}`);
228
+ return incomplete(`Audit failed: ${error.message}`);
220
229
  }
221
- metrics.judgeCalls = verdict.calls;
222
- return this.route(verdict.findings, verdict.waiting, compiled.checklist.dropped.length, begin, auditID, finish, stale, incomplete);
223
230
  }
224
231
  /** Deterministic routing. Model output only suggests; these branches decide. */
225
232
  route(findings, waiting, dropped, begin, auditID, finish, stale, incomplete) {
package/dist/compiler.js CHANGED
@@ -55,7 +55,8 @@ export function validateRules(raw, sources) {
55
55
  return void dropped.push(`${label}: invalid shape`);
56
56
  // The verbatim quote is the traceability anchor. Models often shorten a path, so it may correct the source.
57
57
  const key = normalize(item.quote);
58
- const holds = (candidate) => normalize(candidate.text).includes(key);
58
+ // The compiler reads redacted text, so a rule that mentions a credential comes back with [REDACTED] in its quote.
59
+ const holds = (candidate) => normalize(candidate.text).includes(key) || normalize(redact(candidate.text)).includes(key);
59
60
  const source = sources.find((candidate) => candidate.path === item.source && holds(candidate)) ?? sources.find(holds);
60
61
  if (!source || !key)
61
62
  return void dropped.push(`${label}: quote not verbatim`);
package/dist/config.js CHANGED
@@ -18,7 +18,8 @@ const BASE = {
18
18
  allowFreeModelDataUse: false,
19
19
  trustSdkPrompts: false,
20
20
  timeoutMs: 240_000,
21
- retries: 2,
21
+ retries: 1,
22
+ auditDeadlineMs: 600_000,
22
23
  retentionDays: 30,
23
24
  };
24
25
  const UI = { enabled: true, footer: true, notifications: "important", auditPanel: true, plainTextIcons: false };
package/dist/judge.js CHANGED
@@ -42,11 +42,17 @@ export function digest(evidence, prior, budgetChars, rawIDs = new Set(), preambl
42
42
  const history = evidence.history ?? [];
43
43
  const historyLines = [];
44
44
  let used = 0;
45
+ let full = false;
45
46
  for (const event of [...history].reverse()) {
46
- const line = eventLine(event, rawIDs.has(event.id) ? budgetChars : HISTORY_CLIP);
47
- if (used + line.length > budgetChars * HISTORY_SHARE)
48
- break;
49
- used += line.length;
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;
50
56
  historyLines.unshift(line);
51
57
  }
52
58
  const omitted = history.length - historyLines.length;
@@ -38,6 +38,10 @@ export interface RetryOptions {
38
38
  retries: number;
39
39
  timeoutMs: number;
40
40
  baseDelayMs?: number;
41
+ /** Whole-audit deadline, so retries cannot outlast the host's hook timeout. */
42
+ deadlineAt?: number;
43
+ /** A failed attempt can still have been billed. */
44
+ onFailedCost?: (costUSD: number) => void;
41
45
  }
42
46
  /** Bounded retries with backoff and a timeout. Auth, availability and quota errors fail at once. */
43
47
  export declare function withRetry<T>(run: () => Promise<T>, options: RetryOptions): Promise<T>;
package/dist/providers.js CHANGED
@@ -6,15 +6,22 @@ export async function withRetry(run, options) {
6
6
  for (let attempt = 0; attempt <= options.retries; attempt++) {
7
7
  if (attempt > 0)
8
8
  await new Promise((done) => setTimeout(done, (options.baseDelayMs ?? 1000) * 2 ** (attempt - 1)));
9
+ const left = (options.deadlineAt ?? Infinity) - Date.now();
10
+ if (left <= 0)
11
+ throw lastError ?? new Error("the audit time limit was reached");
12
+ const limit = Math.min(options.timeoutMs, left);
9
13
  let timer;
10
14
  try {
11
15
  const timeout = new Promise((_, reject) => {
12
- timer = setTimeout(() => reject(new Error(`timed out after ${options.timeoutMs} ms`)), options.timeoutMs);
16
+ timer = setTimeout(() => reject(new Error(`timed out after ${limit} ms`)), limit);
13
17
  });
14
18
  return await Promise.race([run(), timeout]);
15
19
  }
16
20
  catch (error) {
17
21
  lastError = error;
22
+ const cost = error?.costUSD;
23
+ if (typeof cost === "number" && cost > 0)
24
+ options.onFailedCost?.(cost);
18
25
  if (HARD_FAILURE.test(String(error?.message ?? error)))
19
26
  break;
20
27
  }
@@ -118,9 +125,15 @@ export function textDecisions(generate, id) {
118
125
  : `- ${question.name} (choice): ${question.instructions} Values: ${question.choices.map((choice) => `${choice.value} = ${choice.description}`).join("; ")}`),
119
126
  ].join("\n");
120
127
  const output = generated(await generate(prompt, DECISIONS_SCHEMA));
121
- const parsed = parseJson(output.text);
122
- if (!parsed || typeof parsed.answers !== "object")
123
- throw new Error("decision output lacks answers");
128
+ let parsed;
129
+ try {
130
+ parsed = parseJson(output.text);
131
+ if (!parsed || typeof parsed.answers !== "object")
132
+ throw new Error("decision output lacks answers");
133
+ }
134
+ catch (error) {
135
+ throw Object.assign(error, { costUSD: output.costUSD });
136
+ }
124
137
  const answers = {};
125
138
  for (const question of questions) {
126
139
  const answer = parsed.answers[question.name];
package/dist/types.d.ts CHANGED
@@ -164,6 +164,8 @@ export interface Config {
164
164
  trustSdkPrompts: boolean;
165
165
  timeoutMs: number;
166
166
  retries: number;
167
+ /** Whole-audit limit. Must stay below the host's hook timeout. */
168
+ auditDeadlineMs: number;
167
169
  retentionDays: number;
168
170
  ui: {
169
171
  enabled: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fidelis",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",