fidelis 0.0.0-stage → 0.1.0

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/store.js ADDED
@@ -0,0 +1,164 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { appendFileSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { homedir, platform } from "node:os";
4
+ import { join } from "node:path";
5
+ export function sha256(text) {
6
+ return createHash("sha256").update(text).digest("hex");
7
+ }
8
+ /** Unambiguous encoding: every value carries its type and byte length. */
9
+ export function canonical(value) {
10
+ if (typeof value === "string")
11
+ return `s${Buffer.byteLength(value)}:${value}`;
12
+ if (Array.isArray(value))
13
+ return `a${value.length}:${value.map(canonical).join("")}`;
14
+ if (value && typeof value === "object") {
15
+ const keys = Object.keys(value).filter((key) => value[key] !== undefined).sort();
16
+ return `o${keys.length}:${keys.map((key) => canonical(key) + canonical(value[key])).join("")}`;
17
+ }
18
+ return `v${String(value)};`;
19
+ }
20
+ export function baseDir(env = process.env) {
21
+ if (env.FIDELIS_HOME)
22
+ return env.FIDELIS_HOME;
23
+ if (platform() === "win32")
24
+ return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "fidelis");
25
+ if (platform() === "darwin")
26
+ return join(homedir(), "Library", "Caches", "fidelis");
27
+ return join(env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "fidelis");
28
+ }
29
+ /** Reversible and path-safe, including `.`, `..` and separators. */
30
+ export function safeID(id) {
31
+ return encodeURIComponent(id).replace(/[.*!'()~]/g, (ch) => `%${ch.charCodeAt(0).toString(16)}`) || "%empty";
32
+ }
33
+ function writeAtomic(path, data) {
34
+ const tmp = `${path}.${randomUUID()}.tmp`;
35
+ writeFileSync(tmp, data, { mode: 0o600 });
36
+ renameSync(tmp, path);
37
+ }
38
+ function readJson(path) {
39
+ try {
40
+ return JSON.parse(readFileSync(path, "utf8"));
41
+ }
42
+ catch (error) {
43
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
44
+ return undefined;
45
+ throw error;
46
+ }
47
+ }
48
+ const LOCK_STALE_MS = 30_000;
49
+ const LOCK_WAIT_MS = 5_000;
50
+ function lock(path) {
51
+ const deadline = Date.now() + LOCK_WAIT_MS;
52
+ for (;;) {
53
+ try {
54
+ mkdirSync(path, { mode: 0o700 });
55
+ return;
56
+ }
57
+ catch (error) {
58
+ if (error.code !== "EEXIST")
59
+ throw error;
60
+ const age = (() => {
61
+ try {
62
+ return Date.now() - statSync(path).mtimeMs;
63
+ }
64
+ catch {
65
+ return 0;
66
+ }
67
+ })();
68
+ if (age > LOCK_STALE_MS)
69
+ rmSync(path, { recursive: true, force: true });
70
+ else if (Date.now() > deadline)
71
+ throw new Error(`fidelis: session lock busy: ${path}`);
72
+ else
73
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
74
+ }
75
+ }
76
+ }
77
+ function newState(host, sessionID) {
78
+ return { host, sessionID, state: "READY", phase: null, revision: 0, updatedAt: 0, turn: 0, corrections: 0, humanIDs: [], boundaries: [], disclosed: [], findings: [], spentUSD: 0 };
79
+ }
80
+ export class Store {
81
+ dir;
82
+ host;
83
+ constructor(host, dir = baseDir()) {
84
+ this.host = host;
85
+ this.dir = dir;
86
+ mkdirSync(join(dir, "checklists"), { recursive: true, mode: 0o700 });
87
+ }
88
+ sessionDir(sessionID) {
89
+ const dir = join(this.dir, "sessions", safeID(this.host), safeID(sessionID));
90
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
91
+ return dir;
92
+ }
93
+ getChecklist(fingerprint) {
94
+ return readJson(join(this.dir, "checklists", `${safeID(fingerprint)}.json`));
95
+ }
96
+ putChecklist(checklist) {
97
+ writeAtomic(join(this.dir, "checklists", `${safeID(checklist.fingerprint)}.json`), JSON.stringify(checklist));
98
+ }
99
+ read(sessionID) {
100
+ return readJson(join(this.sessionDir(sessionID), "state.json")) ?? newState(this.host, sessionID);
101
+ }
102
+ /** Read-modify-write under a short per-session lock. Never held across a model call. */
103
+ update(sessionID, change) {
104
+ const dir = this.sessionDir(sessionID);
105
+ const lockPath = join(dir, "lock");
106
+ lock(lockPath);
107
+ try {
108
+ const state = this.read(sessionID);
109
+ change(state);
110
+ state.revision += 1;
111
+ state.updatedAt = Date.now();
112
+ writeAtomic(join(dir, "state.json"), JSON.stringify(state));
113
+ return state;
114
+ }
115
+ finally {
116
+ rmSync(lockPath, { recursive: true, force: true });
117
+ }
118
+ }
119
+ appendAudit(sessionID, record) {
120
+ appendFileSync(join(this.sessionDir(sessionID), "audits.jsonl"), JSON.stringify(record) + "\n", { mode: 0o600 });
121
+ }
122
+ audits(sessionID, limit = 50) {
123
+ try {
124
+ const lines = readFileSync(join(this.sessionDir(sessionID), "audits.jsonl"), "utf8").trim().split("\n");
125
+ return lines.slice(-limit).flatMap((line) => {
126
+ try {
127
+ return [JSON.parse(line)];
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ });
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ }
138
+ /** Evidence that must survive host compaction. Holds redacted, clipped events by default. */
139
+ readLedger(sessionID) {
140
+ return readJson(join(this.sessionDir(sessionID), "ledger.json")) ?? [];
141
+ }
142
+ writeLedger(sessionID, events) {
143
+ writeAtomic(join(this.sessionDir(sessionID), "ledger.json"), JSON.stringify(events));
144
+ }
145
+ cleanup(retentionDays, now = Date.now()) {
146
+ const cutoff = now - retentionDays * 86_400_000;
147
+ const prune = (dir) => {
148
+ let names = [];
149
+ try {
150
+ names = readdirSync(dir);
151
+ }
152
+ catch {
153
+ return;
154
+ }
155
+ for (const name of names) {
156
+ const path = join(dir, name);
157
+ if (statSync(path).mtimeMs < cutoff)
158
+ rmSync(path, { recursive: true, force: true });
159
+ }
160
+ };
161
+ prune(join(this.dir, "checklists"));
162
+ prune(join(this.dir, "sessions", safeID(this.host)));
163
+ }
164
+ }
@@ -0,0 +1,175 @@
1
+ export type Host = "opencode" | "claude-code";
2
+ export interface InstructionSource {
3
+ path: string;
4
+ scope: string;
5
+ /** Host load order. Later wins when the host defines precedence. */
6
+ priority: number;
7
+ text: string;
8
+ }
9
+ export declare const OBLIGATIONS: readonly ["MUST", "MUST_NOT", "CONDITIONAL", "TEMPORAL", "STOP_CONDITION", "OTHER"];
10
+ export type Obligation = (typeof OBLIGATIONS)[number];
11
+ export interface Rule {
12
+ id: string;
13
+ quote: string;
14
+ source: string;
15
+ obligation: Obligation;
16
+ applicability: string;
17
+ trigger: string;
18
+ deadline: string;
19
+ exceptions: string[];
20
+ expectedEvidence: string;
21
+ counterEvidence: string;
22
+ repairHint: string;
23
+ priority: number;
24
+ supersedable: boolean;
25
+ supersededBy: string | null;
26
+ conflictsWith: string[];
27
+ }
28
+ export interface Checklist {
29
+ schema: number;
30
+ fingerprint: string;
31
+ model: string;
32
+ compiledAt: number;
33
+ rules: Rule[];
34
+ /** Model output rejected by validation, kept for traceability. */
35
+ dropped: string[];
36
+ }
37
+ export type EventKind = "user" | "assistant" | "tool" | "fidelis" | "system";
38
+ export type EventStatus = "completed" | "failed" | "running" | "cancelled" | "unknown";
39
+ export interface ExecEvent {
40
+ id: string;
41
+ seq: number;
42
+ at?: number;
43
+ kind: EventKind;
44
+ actor?: "agent" | "subagent";
45
+ name?: string;
46
+ input?: unknown;
47
+ output?: string;
48
+ text?: string;
49
+ status?: EventStatus;
50
+ }
51
+ export interface Evidence {
52
+ userRequest: string;
53
+ /** The turn under audit: from the opening prompt through every corrective turn. */
54
+ events: ExecEvent[];
55
+ /** Earlier turns. Context for temporal rules, never a source of new accusations. */
56
+ history?: ExecEvent[];
57
+ finalReply: string;
58
+ /** True only when the source is known to hold every event of the window. */
59
+ complete: boolean;
60
+ gaps: string[];
61
+ }
62
+ export type RuleStatus = "NOT_APPLICABLE" | "PENDING" | "SATISFIED" | "VIOLATED" | "UNKNOWN";
63
+ export type Repair = "safe" | "user" | "unrepairable";
64
+ export interface Finding {
65
+ ruleID: string;
66
+ status: RuleStatus;
67
+ quote: string;
68
+ source: string;
69
+ obligation: Obligation;
70
+ explanation: string;
71
+ evidence: string[];
72
+ severity: "high" | "low";
73
+ probability?: number;
74
+ confidenceKind?: "decision" | "self-reported";
75
+ uncertainty?: string;
76
+ repair?: Repair;
77
+ repairHint?: string;
78
+ /** The breach stays on record; later events show it was repaired or honestly acknowledged. */
79
+ resolved?: boolean;
80
+ }
81
+ export type StateName = "READY" | "RUNNING" | "AUDITING" | "CORRECTING" | "WAITING_USER" | "APPROVED" | "AUDIT_INCOMPLETE" | "STOPPED";
82
+ export type Phase = "compiling" | "judging" | null;
83
+ export interface SessionState {
84
+ host: Host;
85
+ sessionID: string;
86
+ state: StateName;
87
+ phase: Phase;
88
+ reason?: string;
89
+ revision: number;
90
+ updatedAt: number;
91
+ turn: number;
92
+ corrections: number;
93
+ humanIDs: string[];
94
+ boundaries: string[];
95
+ audit?: {
96
+ id: string;
97
+ boundaryID: string;
98
+ turn: number;
99
+ startedAt: number;
100
+ };
101
+ lastAuditID?: string;
102
+ /** Rule IDs already reported as unrepairable: disclose once, never retry. */
103
+ disclosed: string[];
104
+ violationKey?: string;
105
+ findings: Finding[];
106
+ spentUSD: number;
107
+ }
108
+ export interface AuditMetrics {
109
+ cacheHit: boolean;
110
+ compilerCalls: number;
111
+ judgeCalls: number;
112
+ tokens: number;
113
+ costUSD: number;
114
+ latencyMs: number;
115
+ uncertain: number;
116
+ }
117
+ export interface AuditRecord {
118
+ auditID: string;
119
+ boundaryID: string;
120
+ turn: number;
121
+ at: number;
122
+ state: StateName;
123
+ reason?: string;
124
+ findings: Finding[];
125
+ metrics: AuditMetrics;
126
+ corrections: number;
127
+ }
128
+ export type Outcome = {
129
+ action: "none";
130
+ state: StateName;
131
+ duplicate?: boolean;
132
+ reason?: string;
133
+ notice?: string;
134
+ } | {
135
+ action: "correct";
136
+ state: StateName;
137
+ auditID: string;
138
+ text: string;
139
+ } | {
140
+ action: "wait";
141
+ state: StateName;
142
+ auditID: string;
143
+ reason: string;
144
+ notice?: string;
145
+ };
146
+ export interface Config {
147
+ enabled: boolean;
148
+ compilerModel: string;
149
+ compilerEffort: string;
150
+ judgeModel: string;
151
+ judgeEffort: string;
152
+ compilerPromptPath: string | null;
153
+ judgePromptPath: string | null;
154
+ auditOnCompletion: boolean;
155
+ maxCorrections: number;
156
+ minViolationConfidence: number;
157
+ maxAuditTokens: number;
158
+ maxAuditCostUSD: number | null;
159
+ cacheChecklist: boolean;
160
+ storeRawTranscript: boolean;
161
+ /** Explicit consent to send rules and evidence to a free model whose data may train it. */
162
+ allowFreeModelDataUse: boolean;
163
+ /** Accept SDK-origin prompts as human replies. Off: only verified human records release a wait. */
164
+ trustSdkPrompts: boolean;
165
+ timeoutMs: number;
166
+ retries: number;
167
+ retentionDays: number;
168
+ ui: {
169
+ enabled: boolean;
170
+ footer: boolean;
171
+ notifications: "important" | "all" | "off";
172
+ auditPanel: boolean;
173
+ plainTextIcons: boolean;
174
+ };
175
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export const OBLIGATIONS = ["MUST", "MUST_NOT", "CONDITIONAL", "TEMPORAL", "STOP_CONDITION", "OTHER"];
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "fidelis",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Harness-agnostic AI agent auditor core: checklist compiler, judge pipeline and session state machine.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": { "type": "git", "url": "git+https://github.com/insign/fidelis.git", "directory": "packages/core" },
8
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
+ "files": ["dist", "README.md"],
10
+ "publishConfig": { "access": "public" },
11
+ "engines": { "node": ">=22.18" }
12
+ }