@salucallc/tiresias-sdk 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/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @salucallc/tiresias-sdk
3
+ * Unified Tiresias AI Safety SDK
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { Tiresias, OpenAIAdapter } from "@salucallc/tiresias-sdk";
8
+ *
9
+ * const tiresias = new Tiresias();
10
+ * const adapter = new OpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY! });
11
+ *
12
+ * const result = await tiresias.call(
13
+ * [{ role: "user", content: "What is the capital of France?" }],
14
+ * adapter,
15
+ * { sessionId: "user-123" }
16
+ * );
17
+ *
18
+ * if (result.blocked) {
19
+ * console.log("Request blocked:", result.decision, result.composite.score);
20
+ * } else {
21
+ * console.log(result.llmResponse?.content);
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ export { Tiresias } from "./tiresias.js";
27
+ export { computeCompositeScore, scoreToPolicy } from "./scoring.js";
28
+ export {
29
+ computeChainHash,
30
+ hashRequest,
31
+ buildAuditEntry,
32
+ verifyChain,
33
+ MemoryAuditStore,
34
+ GENESIS_HASH,
35
+ } from "./audit.js";
36
+ export { SessionManager } from "./session.js";
37
+ export { OpenAIAdapter, AnthropicAdapter } from "./adapters.js";
38
+ export type {
39
+ WatchSignal,
40
+ ReportSignal,
41
+ NetworkSignal,
42
+ CompositeScore,
43
+ PolicyDecision,
44
+ PolicyThresholds,
45
+ AuditEntry,
46
+ TiresiasSession,
47
+ Message,
48
+ MessageRole,
49
+ LLMResponse,
50
+ LLMAdapter,
51
+ LLMCallOptions,
52
+ TiresiasConfig,
53
+ AuditStore,
54
+ TiresiasCallResult,
55
+ } from "./types.js";
56
+ export { DEFAULT_POLICY_THRESHOLDS } from "./types.js";
package/src/scoring.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * PATENT-CRITICAL: Composite Risk Scoring (SALUCA-005 / cross_layer_orchestration)
3
+ *
4
+ * Composite formula:
5
+ * score = (0.5 × injection_threat) + (0.4 × pii_risk) + (0.1 × network_threat)
6
+ *
7
+ * This cross-module weighting is the novel IP element of the orchestration patent.
8
+ * The injection signal dominates (0.5) because it represents active adversarial attack;
9
+ * PII leakage is structural risk (0.4); network telemetry is ambient context (0.1).
10
+ */
11
+
12
+ import type {
13
+ WatchSignal,
14
+ ReportSignal,
15
+ NetworkSignal,
16
+ CompositeScore,
17
+ PolicyDecision,
18
+ PolicyThresholds,
19
+ } from "./types.js";
20
+ import { DEFAULT_POLICY_THRESHOLDS } from "./types.js";
21
+
22
+ // Composite weight constants — PATENT-CRITICAL
23
+ const W_INJECTION = 0.5;
24
+ const W_PII = 0.4;
25
+ const W_NETWORK = 0.1;
26
+
27
+ /**
28
+ * Compute composite risk score from three independent safety signals.
29
+ * PATENT-CRITICAL: formula and weights are novel claims in SALUCA-005.
30
+ */
31
+ export function computeCompositeScore(
32
+ watch: WatchSignal,
33
+ report: ReportSignal,
34
+ network: NetworkSignal,
35
+ thresholds: PolicyThresholds = DEFAULT_POLICY_THRESHOLDS,
36
+ ): CompositeScore {
37
+ const score = clamp(
38
+ W_INJECTION * watch.injectionThreat +
39
+ W_PII * report.piiRisk +
40
+ W_NETWORK * network.networkThreat,
41
+ );
42
+
43
+ const policy = scoreToPolicy(score, thresholds);
44
+
45
+ return {
46
+ score,
47
+ injectionThreat: watch.injectionThreat,
48
+ piiRisk: report.piiRisk,
49
+ networkThreat: network.networkThreat,
50
+ policy,
51
+ timestamp: Date.now(),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Map composite score to policy decision using configured thresholds.
57
+ * Default thresholds are patent-critical nominal values.
58
+ */
59
+ export function scoreToPolicy(
60
+ score: number,
61
+ thresholds: PolicyThresholds = DEFAULT_POLICY_THRESHOLDS,
62
+ ): PolicyDecision {
63
+ if (score >= thresholds.quarantineAt) return "QUARANTINE";
64
+ if (score >= thresholds.blockAt) return "BLOCK";
65
+ if (score >= thresholds.warnAt) return "WARN";
66
+ return "ALLOW";
67
+ }
68
+
69
+ function clamp(v: number, lo = 0, hi = 1): number {
70
+ return Math.min(Math.max(v, lo), hi);
71
+ }
package/src/session.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tiresias Session Manager
3
+ * Tracks per-session state: request count, last score, audit chain head.
4
+ */
5
+
6
+ import { randomUUID } from "node:crypto";
7
+ import type { TiresiasSession, CompositeScore } from "./types.js";
8
+ import { GENESIS_HASH } from "./audit.js";
9
+
10
+ export class SessionManager {
11
+ private sessions: Map<string, TiresiasSession> = new Map();
12
+
13
+ create(sessionId?: string): TiresiasSession {
14
+ const id = sessionId ?? randomUUID();
15
+ const session: TiresiasSession = {
16
+ sessionId: id,
17
+ createdAt: Date.now(),
18
+ requestCount: 0,
19
+ lastScore: null,
20
+ auditChainHead: GENESIS_HASH,
21
+ };
22
+ this.sessions.set(id, session);
23
+ return session;
24
+ }
25
+
26
+ get(sessionId: string): TiresiasSession | undefined {
27
+ return this.sessions.get(sessionId);
28
+ }
29
+
30
+ getOrCreate(sessionId: string): TiresiasSession {
31
+ return this.sessions.get(sessionId) ?? this.create(sessionId);
32
+ }
33
+
34
+ update(sessionId: string, score: CompositeScore, newChainHash: string): void {
35
+ const session = this.getOrCreate(sessionId);
36
+ session.requestCount += 1;
37
+ session.lastScore = score;
38
+ session.auditChainHead = newChainHash;
39
+ this.sessions.set(sessionId, session);
40
+ }
41
+
42
+ list(): TiresiasSession[] {
43
+ return Array.from(this.sessions.values());
44
+ }
45
+
46
+ delete(sessionId: string): boolean {
47
+ return this.sessions.delete(sessionId);
48
+ }
49
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Tiresias SDK — Main Orchestrator
3
+ *
4
+ * tiresias.call(messages, adapter, options) is the high-level entry point.
5
+ * It runs the three safety signals, computes composite score, applies policy,
6
+ * writes a hash-chained audit entry, and either passes through to the LLM
7
+ * or blocks the request.
8
+ */
9
+
10
+ import type {
11
+ Message,
12
+ LLMAdapter,
13
+ LLMCallOptions,
14
+ TiresiasCallResult,
15
+ TiresiasConfig,
16
+ WatchSignal,
17
+ ReportSignal,
18
+ NetworkSignal,
19
+ } from "./types.js";
20
+ import { DEFAULT_POLICY_THRESHOLDS } from "./types.js";
21
+ import { computeCompositeScore } from "./scoring.js";
22
+ import { buildAuditEntry, MemoryAuditStore } from "./audit.js";
23
+ import { SessionManager } from "./session.js";
24
+
25
+ // Lazily import sub-modules to allow tree-shaking
26
+ import {
27
+ generateKey,
28
+ injectGuard,
29
+ stripEcho,
30
+ verify,
31
+ } from "@salucallc/tiresias-watch";
32
+
33
+ import { scan } from "@salucallc/tiresias-report";
34
+
35
+ export class Tiresias {
36
+ private config: TiresiasConfig;
37
+ private sessions: SessionManager;
38
+ private auditStore: MemoryAuditStore;
39
+
40
+ constructor(config: TiresiasConfig = {}) {
41
+ this.config = config;
42
+ this.sessions = new SessionManager();
43
+ this.auditStore = (config.auditStore as MemoryAuditStore) ?? new MemoryAuditStore();
44
+ }
45
+
46
+ /**
47
+ * Main entry point.
48
+ * 1. Run watch (injection detection)
49
+ * 2. Run report (PII scan)
50
+ * 3. Get network signal (passthrough or provider)
51
+ * 4. Compute composite score
52
+ * 5. Apply policy
53
+ * 6. Write hash-chained audit entry
54
+ * 7. If ALLOW/WARN: call LLM adapter and return response
55
+ * If BLOCK/QUARANTINE: return without calling LLM
56
+ */
57
+ async call(
58
+ messages: Message[],
59
+ adapter: LLMAdapter,
60
+ options: LLMCallOptions & { sessionId?: string } = {},
61
+ ): Promise<TiresiasCallResult> {
62
+ const { sessionId: sid, ...llmOptions } = options;
63
+ const session = this.sessions.getOrCreate(sid ?? "default");
64
+
65
+ // Serialize request for hashing and scanning
66
+ const requestContent = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
67
+
68
+ // ── Signal 1: Injection Detection (tiresias-watch) ────────────────────────
69
+ const key = generateKey();
70
+ const guardedMessages = messages.map((m) => ({
71
+ ...m,
72
+ content: injectGuard(m.content, key),
73
+ }));
74
+ // We scan the original content for injection markers from prior round-trips.
75
+ // For a fresh request, injection threat comes from structural analysis.
76
+ const watchSignal = await this.runWatchSignal(messages, key);
77
+
78
+ // ── Signal 2: PII Detection (tiresias-report) ─────────────────────────────
79
+ const reportSignal = await this.runReportSignal(requestContent);
80
+
81
+ // ── Signal 3: Network Threat (tiresias-network or stub) ───────────────────
82
+ const networkSignal = await this.runNetworkSignal();
83
+
84
+ // ── Composite Score (PATENT-CRITICAL) ─────────────────────────────────────
85
+ const thresholds = this.config.policy ?? DEFAULT_POLICY_THRESHOLDS;
86
+ const composite = computeCompositeScore(watchSignal, reportSignal, networkSignal, thresholds);
87
+
88
+ // ── Audit Entry (PATENT-CRITICAL hash chain) ──────────────────────────────
89
+ const prevHash = await this.auditStore.getHead(session.sessionId);
90
+ const auditEntry = buildAuditEntry(
91
+ session.sessionId,
92
+ requestContent,
93
+ composite,
94
+ prevHash,
95
+ );
96
+ await this.auditStore.append(auditEntry);
97
+ this.sessions.update(session.sessionId, composite, auditEntry.chainHash);
98
+
99
+ const blocked = composite.policy === "BLOCK" || composite.policy === "QUARANTINE";
100
+
101
+ // ── Policy Callbacks ──────────────────────────────────────────────────────
102
+ if (composite.policy === "WARN" && this.config.onWarn) {
103
+ await this.config.onWarn(auditEntry);
104
+ }
105
+ if (blocked && this.config.onBlock) {
106
+ await this.config.onBlock(auditEntry);
107
+ }
108
+
109
+ // ── LLM Call ──────────────────────────────────────────────────────────────
110
+ let llmResponse;
111
+ if (!blocked) {
112
+ // Use guarded messages so we can detect echo manipulation in future turns
113
+ llmResponse = await adapter.call(guardedMessages, llmOptions);
114
+
115
+ // Strip guard tokens from response before returning to caller
116
+ if (llmResponse.content) {
117
+ llmResponse = {
118
+ ...llmResponse,
119
+ content: stripEcho(llmResponse.content),
120
+ };
121
+ }
122
+ }
123
+
124
+ return {
125
+ decision: composite.policy,
126
+ composite,
127
+ auditEntry,
128
+ llmResponse,
129
+ blocked,
130
+ };
131
+ }
132
+
133
+ /** Audit log access */
134
+ get audit(): MemoryAuditStore {
135
+ return this.auditStore;
136
+ }
137
+
138
+ /** Session access */
139
+ get session(): SessionManager {
140
+ return this.sessions;
141
+ }
142
+
143
+ // ── Private signal runners ────────────────────────────────────────────────
144
+
145
+ private async runWatchSignal(messages: Message[], key: string): Promise<WatchSignal> {
146
+ // For a fresh outbound request, injection threat is determined by
147
+ // checking if any inbound message contains injection markers aimed at
148
+ // hijacking the guard echo. We use verifyResponse on each user message.
149
+ let maxThreat = 0;
150
+ let echoFound = false;
151
+ let failureMode: string | undefined;
152
+
153
+ for (const m of messages) {
154
+ if (m.role !== "user") continue;
155
+ const result = verify(m.content, key);
156
+ if (!result.verified) {
157
+ if (result.failure_mode) {
158
+ failureMode = result.failure_mode;
159
+ maxThreat = Math.max(maxThreat, result.threat);
160
+ echoFound = true;
161
+ }
162
+ }
163
+ }
164
+
165
+ // Check for common injection patterns in user messages
166
+ const injectionPatterns = [
167
+ /ignore\s+(all\s+)?previous\s+instructions?/i,
168
+ /you\s+are\s+now\s+(?:a\s+)?(?:an?\s+)?(?:different|new|another|alternative)/i,
169
+ /disregard\s+(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions?|rules?|guidelines?)/i,
170
+ /\[SYSTEM\]/i,
171
+ /<\|system\|>/i,
172
+ /###\s*SYSTEM/i,
173
+ ];
174
+
175
+ for (const m of messages) {
176
+ if (m.role !== "user") continue;
177
+ let patternHits = 0;
178
+ for (const p of injectionPatterns) {
179
+ if (p.test(m.content)) patternHits++;
180
+ }
181
+ if (patternHits > 0) {
182
+ maxThreat = Math.max(maxThreat, Math.min(0.3 + patternHits * 0.2, 1.0));
183
+ }
184
+ }
185
+
186
+ return { injectionThreat: maxThreat, echoFound, failureMode };
187
+ }
188
+
189
+ private async runReportSignal(content: string): Promise<ReportSignal> {
190
+ try {
191
+ const result = await scan(content);
192
+ const highConf = result.entities.filter((e) => e.confidence > 0.85).length;
193
+ return {
194
+ piiRisk: result.risk,
195
+ entityCount: result.entity_count,
196
+ highConfidenceCount: highConf,
197
+ };
198
+ } catch {
199
+ // Fail safe: treat scan failure as zero risk (don't block on scan error)
200
+ return { piiRisk: 0, entityCount: 0, highConfidenceCount: 0 };
201
+ }
202
+ }
203
+
204
+ private async runNetworkSignal(): Promise<NetworkSignal> {
205
+ if (this.config.networkThreatProvider) {
206
+ try {
207
+ const threat = await this.config.networkThreatProvider();
208
+ return { networkThreat: Math.min(Math.max(threat, 0), 1) };
209
+ } catch {
210
+ return { networkThreat: 0 };
211
+ }
212
+ }
213
+ return { networkThreat: 0 };
214
+ }
215
+ }
package/src/types.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Tiresias SDK — Shared Types
3
+ * Composite scoring, policy engine, session, and audit types.
4
+ */
5
+
6
+ // ── Signals from sub-modules ──────────────────────────────────────────────────
7
+
8
+ export interface WatchSignal {
9
+ injectionThreat: number; // [0,1] — from tiresias-watch
10
+ echoFound: boolean;
11
+ failureMode?: string;
12
+ }
13
+
14
+ export interface ReportSignal {
15
+ piiRisk: number; // [0,1] — from tiresias-report
16
+ entityCount: number;
17
+ highConfidenceCount: number;
18
+ }
19
+
20
+ export interface NetworkSignal {
21
+ networkThreat: number; // [0,1] — from tiresias-network (future)
22
+ }
23
+
24
+ // ── Composite Scoring ─────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * PATENT-CRITICAL: Composite risk score formula (SALUCA-005 / cross_layer_orchestration)
28
+ * score = (0.5 × injection_threat) + (0.4 × pii_risk) + (0.1 × network_threat)
29
+ */
30
+ export interface CompositeScore {
31
+ score: number; // [0,1]
32
+ injectionThreat: number;
33
+ piiRisk: number;
34
+ networkThreat: number;
35
+ policy: PolicyDecision;
36
+ timestamp: number; // Unix ms
37
+ }
38
+
39
+ // ── Policy Engine ─────────────────────────────────────────────────────────────
40
+
41
+ export type PolicyDecision = "ALLOW" | "WARN" | "BLOCK" | "QUARANTINE";
42
+
43
+ /**
44
+ * Policy thresholds (configurable, defaults are patent-critical values)
45
+ * ALLOW < 0.30
46
+ * WARN < 0.60
47
+ * BLOCK < 0.85
48
+ * QUARANTINE >= 0.85
49
+ */
50
+ export interface PolicyThresholds {
51
+ warnAt: number; // default 0.30
52
+ blockAt: number; // default 0.60
53
+ quarantineAt: number; // default 0.85
54
+ }
55
+
56
+ export const DEFAULT_POLICY_THRESHOLDS: PolicyThresholds = {
57
+ warnAt: 0.30,
58
+ blockAt: 0.60,
59
+ quarantineAt: 0.85,
60
+ };
61
+
62
+ // ── Audit Log ─────────────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * PATENT-CRITICAL: Hash-chained audit log entry (SALUCA-005)
66
+ * chain_hash = SHA3-256(prev_chain_hash || session_id || timestamp || score_json)
67
+ */
68
+ export interface AuditEntry {
69
+ entryId: string; // UUID v4
70
+ sessionId: string;
71
+ timestamp: number; // Unix ms
72
+ requestHash: string; // SHA-256 of raw request content
73
+ composite: CompositeScore;
74
+ chainHash: string; // SHA3-256 chain link — PATENT-CRITICAL
75
+ prevChainHash: string; // "genesis" for first entry
76
+ metadata?: Record<string, unknown>;
77
+ }
78
+
79
+ // ── Session ───────────────────────────────────────────────────────────────────
80
+
81
+ export interface TiresiasSession {
82
+ sessionId: string;
83
+ createdAt: number;
84
+ requestCount: number;
85
+ lastScore: CompositeScore | null;
86
+ auditChainHead: string; // most recent chain hash
87
+ }
88
+
89
+ // ── LLM Adapters ─────────────────────────────────────────────────────────────
90
+
91
+ export type MessageRole = "system" | "user" | "assistant";
92
+
93
+ export interface Message {
94
+ role: MessageRole;
95
+ content: string;
96
+ }
97
+
98
+ export interface LLMResponse {
99
+ content: string;
100
+ usage?: {
101
+ inputTokens: number;
102
+ outputTokens: number;
103
+ };
104
+ raw?: unknown;
105
+ }
106
+
107
+ export interface LLMAdapter {
108
+ call(messages: Message[], options?: LLMCallOptions): Promise<LLMResponse>;
109
+ }
110
+
111
+ export interface LLMCallOptions {
112
+ model?: string;
113
+ maxTokens?: number;
114
+ temperature?: number;
115
+ systemPrompt?: string;
116
+ }
117
+
118
+ // ── Tiresias SDK Config ───────────────────────────────────────────────────────
119
+
120
+ export interface TiresiasConfig {
121
+ policy?: PolicyThresholds;
122
+ auditStore?: AuditStore;
123
+ networkThreatProvider?: () => Promise<number>;
124
+ onBlock?: (entry: AuditEntry) => void | Promise<void>;
125
+ onWarn?: (entry: AuditEntry) => void | Promise<void>;
126
+ }
127
+
128
+ // ── Audit Store Interface ─────────────────────────────────────────────────────
129
+
130
+ export interface AuditStore {
131
+ append(entry: AuditEntry): Promise<void>;
132
+ getHead(sessionId: string): Promise<string>; // returns latest chain hash
133
+ query(sessionId: string, limit?: number): Promise<AuditEntry[]>;
134
+ }
135
+
136
+ // ── Result from tiresias.call() ───────────────────────────────────────────────
137
+
138
+ export interface TiresiasCallResult {
139
+ decision: PolicyDecision;
140
+ composite: CompositeScore;
141
+ auditEntry: AuditEntry;
142
+ llmResponse?: LLMResponse; // undefined if BLOCK or QUARANTINE
143
+ blocked: boolean;
144
+ }