@turingpulse/sdk 1.0.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/.github/dependabot.yml +38 -0
- package/.github/workflows/ci.yml +246 -0
- package/.github/workflows/framework-compat.yml +169 -0
- package/.github/workflows/security.yml +336 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +13 -0
- package/MIGRATION.md +30 -0
- package/README.md +221 -0
- package/dist/attachments.d.ts +28 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +59 -0
- package/dist/attachments.js.map +1 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +126 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +163 -0
- package/dist/context.js.map +1 -0
- package/dist/decorators.d.ts +6 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +52 -0
- package/dist/decorators.js.map +1 -0
- package/dist/deploy.d.ts +89 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +203 -0
- package/dist/deploy.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/eventBuilder.d.ts +21 -0
- package/dist/eventBuilder.d.ts.map +1 -0
- package/dist/eventBuilder.js +127 -0
- package/dist/eventBuilder.js.map +1 -0
- package/dist/fingerprint.d.ts +158 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +339 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/governance.d.ts +47 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +104 -0
- package/dist/governance.js.map +1 -0
- package/dist/http.d.ts +62 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +181 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +40 -0
- package/dist/instrumentation.d.ts.map +1 -0
- package/dist/instrumentation.js +31 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/integrations/mastra.d.ts +64 -0
- package/dist/integrations/mastra.d.ts.map +1 -0
- package/dist/integrations/mastra.js +256 -0
- package/dist/integrations/mastra.js.map +1 -0
- package/dist/kpi.d.ts +21 -0
- package/dist/kpi.d.ts.map +1 -0
- package/dist/kpi.js +83 -0
- package/dist/kpi.js.map +1 -0
- package/dist/llmDetector.d.ts +22 -0
- package/dist/llmDetector.d.ts.map +1 -0
- package/dist/llmDetector.js +269 -0
- package/dist/llmDetector.js.map +1 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +312 -0
- package/dist/plugin.js.map +1 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +18 -0
- package/dist/registry.js.map +1 -0
- package/dist/tracing.d.ts +10 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +30 -0
- package/dist/tracing.js.map +1 -0
- package/dist/triggerState.d.ts +5 -0
- package/dist/triggerState.d.ts.map +1 -0
- package/dist/triggerState.js +19 -0
- package/dist/triggerState.js.map +1 -0
- package/dist/utils.d.ts +27 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +72 -0
- package/dist/utils.js.map +1 -0
- package/package.json +37 -0
- package/packages/anthropic/package.json +16 -0
- package/packages/anthropic/src/index.ts +5 -0
- package/packages/anthropic/src/wrapper.ts +102 -0
- package/packages/anthropic/tsconfig.build.json +20 -0
- package/packages/langchain/package.json +16 -0
- package/packages/langchain/src/index.ts +7 -0
- package/packages/langchain/src/wrapper.ts +51 -0
- package/packages/mastra/package.json +17 -0
- package/packages/mastra/src/index.ts +8 -0
- package/packages/mastra/src/wrapper.ts +301 -0
- package/packages/openai/package.json +16 -0
- package/packages/openai/src/index.ts +8 -0
- package/packages/openai/src/wrapper.ts +103 -0
- package/packages/openai/tsconfig.build.json +20 -0
- package/packages/openclaw/openclaw.plugin.json +100 -0
- package/packages/openclaw/package.json +41 -0
- package/packages/openclaw/src/buffer.ts +99 -0
- package/packages/openclaw/src/config.ts +139 -0
- package/packages/openclaw/src/hooks/governance.ts +267 -0
- package/packages/openclaw/src/hooks/lifecycle.ts +75 -0
- package/packages/openclaw/src/hooks/telemetry.ts +207 -0
- package/packages/openclaw/src/index.ts +91 -0
- package/packages/openclaw/src/mapper.ts +233 -0
- package/packages/openclaw/src/session-tracker.ts +181 -0
- package/packages/openclaw/src/types.ts +220 -0
- package/packages/openclaw/tests/buffer.test.ts +148 -0
- package/packages/openclaw/tests/config.test.ts +122 -0
- package/packages/openclaw/tests/governance.test.ts +232 -0
- package/packages/openclaw/tests/mapper.test.ts +242 -0
- package/packages/openclaw/tests/session-tracker.test.ts +124 -0
- package/packages/openclaw/tsconfig.json +18 -0
- package/packages/openclaw/vitest.config.ts +8 -0
- package/packages/vercel-ai/package.json +16 -0
- package/packages/vercel-ai/src/index.ts +5 -0
- package/packages/vercel-ai/src/wrapper.ts +49 -0
- package/scripts/bump-version.sh +58 -0
- package/scripts/update-readme-compat.mjs +151 -0
- package/src/__tests__/fingerprint.test.ts +328 -0
- package/src/attachments.ts +88 -0
- package/src/config.ts +164 -0
- package/src/context.ts +258 -0
- package/src/decorators.ts +61 -0
- package/src/deploy.ts +260 -0
- package/src/errors.ts +44 -0
- package/src/eventBuilder.ts +153 -0
- package/src/fingerprint.ts +421 -0
- package/src/governance.ts +156 -0
- package/src/http.ts +241 -0
- package/src/index.ts +57 -0
- package/src/instrumentation.ts +68 -0
- package/src/integrations/mastra.ts +335 -0
- package/src/kpi.ts +112 -0
- package/src/llmDetector.ts +330 -0
- package/src/plugin.ts +384 -0
- package/src/registry.ts +27 -0
- package/src/tracing.ts +39 -0
- package/src/triggerState.ts +27 -0
- package/src/utils.ts +78 -0
- package/tests/compat/anthropic.test.ts +61 -0
- package/tests/compat/cohere.test.ts +57 -0
- package/tests/compat/google-genai.test.ts +61 -0
- package/tests/compat/langchain-openai.test.ts +41 -0
- package/tests/compat/langchain.test.ts +64 -0
- package/tests/compat/mistral.test.ts +58 -0
- package/tests/compat/openai.test.ts +71 -0
- package/tests/compat/vercel-ai.test.ts +56 -0
- package/tests/plugins/anthropic-wrapper.test.ts +120 -0
- package/tests/plugins/langchain-wrapper.test.ts +128 -0
- package/tests/plugins/openai-wrapper.test.ts +165 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fingerprinting for workflow structure and change detection.
|
|
3
|
+
*
|
|
4
|
+
* The fingerprint builder captures a compact representation of every workflow
|
|
5
|
+
* run so the backend can automatically detect **8 change types** by comparing
|
|
6
|
+
* consecutive fingerprints:
|
|
7
|
+
*
|
|
8
|
+
* 1. structure — node/edge additions or removals
|
|
9
|
+
* 2. config — general configuration changes
|
|
10
|
+
* 3. prompt — system prompt changes on LLM nodes
|
|
11
|
+
* 4. model_change — LLM model identity or generation parameters changed
|
|
12
|
+
* 5. eval_config_change — evaluation / scoring / judging config changed
|
|
13
|
+
* 6. policy_change — guardrail / safety / moderation config changed
|
|
14
|
+
* 7. graph_version_change — (detected server-side from graph_snapshot_id)
|
|
15
|
+
* 8. deploy — (registered externally via CI/CD; not from fingerprint)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { FORBIDDEN_KEYS } from './utils';
|
|
20
|
+
|
|
21
|
+
function sortKeysDeep(obj: unknown, depth = 0): unknown {
|
|
22
|
+
if (depth > 20 || typeof obj !== 'object' || obj === null) return obj;
|
|
23
|
+
if (Array.isArray(obj)) return obj.map((v) => sortKeysDeep(v, depth + 1));
|
|
24
|
+
const sorted: Record<string, unknown> = {};
|
|
25
|
+
for (const k of Object.keys(obj as Record<string, unknown>).sort()) {
|
|
26
|
+
if (!FORBIDDEN_KEYS.has(k)) {
|
|
27
|
+
sorted[k] = sortKeysDeep((obj as Record<string, unknown>)[k], depth + 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return sorted;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sha256hex(data: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return createHash('sha256').update(data, 'utf8').digest('hex');
|
|
36
|
+
} catch {
|
|
37
|
+
let hash = 5381;
|
|
38
|
+
for (let i = 0; i < data.length; i++) {
|
|
39
|
+
hash = ((hash << 5) + hash + data.charCodeAt(i)) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
return hash.toString(16).padStart(16, '0');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Config key classification
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const MODEL_CONFIG_KEYS = new Set([
|
|
50
|
+
// Identity (snake_case + camelCase across all providers)
|
|
51
|
+
'model', 'model_name', 'modelname', 'model_id', 'modelid',
|
|
52
|
+
// Generation parameters — snake_case
|
|
53
|
+
'temperature', 'top_p', 'top_k', 'max_tokens', 'max_output_tokens',
|
|
54
|
+
'frequency_penalty', 'presence_penalty',
|
|
55
|
+
'stop', 'stop_sequences',
|
|
56
|
+
'candidate_count', 'response_format', 'context_window', 'model_kwargs',
|
|
57
|
+
// Generation parameters — camelCase (JS SDKs, Bedrock, Google)
|
|
58
|
+
'maxtokens', 'maxoutputtokens', 'topp', 'topk',
|
|
59
|
+
'frequencypenalty', 'presencepenalty',
|
|
60
|
+
'stopsequences', 'candidatecount', 'responseformat',
|
|
61
|
+
'contextwindow', 'modelkwargs',
|
|
62
|
+
// Cohere aliases (p = top_p, k = top_k)
|
|
63
|
+
'p', 'k',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const EVAL_CONFIG_KEYS = new Set([
|
|
67
|
+
'eval_model', 'eval_prompt', 'eval_criteria', 'eval_rubric',
|
|
68
|
+
'eval_config', 'eval_threshold', 'eval_type', 'eval_template',
|
|
69
|
+
'evaluation_model', 'evaluation_prompt', 'evaluation_criteria',
|
|
70
|
+
'scoring_rubric', 'scoring_model', 'scoring_prompt', 'scoring_criteria',
|
|
71
|
+
'judge_model', 'judge_prompt', 'judge_criteria', 'judge_config',
|
|
72
|
+
'grading_rubric', 'grading_model', 'grading_criteria',
|
|
73
|
+
'rubric', 'criteria', 'judge',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const POLICY_CONFIG_KEYS = new Set([
|
|
77
|
+
'policy', 'policy_config', 'policy_rules', 'policy_model',
|
|
78
|
+
'guard', 'guardrail', 'guardrails', 'guard_config',
|
|
79
|
+
'guardrail_config', 'guardrail_rules',
|
|
80
|
+
'filter', 'content_filter', 'content_filter_config',
|
|
81
|
+
'moderation', 'moderation_config', 'moderation_model',
|
|
82
|
+
'safety', 'safety_config', 'safety_threshold',
|
|
83
|
+
'rate_limit', 'rate_limit_config',
|
|
84
|
+
'max_retries', 'timeout',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Classify a config key into a hash category.
|
|
89
|
+
* Returns one of: "model", "eval", "policy", or "general".
|
|
90
|
+
*/
|
|
91
|
+
export function classifyConfigKey(key: string): string {
|
|
92
|
+
const lower = key.toLowerCase();
|
|
93
|
+
|
|
94
|
+
if (MODEL_CONFIG_KEYS.has(lower)) return 'model';
|
|
95
|
+
if (EVAL_CONFIG_KEYS.has(lower)) return 'eval';
|
|
96
|
+
if (POLICY_CONFIG_KEYS.has(lower)) return 'policy';
|
|
97
|
+
|
|
98
|
+
// Prefix-based fallback
|
|
99
|
+
if (/^(eval_|evaluation_|scoring_|judge_|grading_)/.test(lower)) return 'eval';
|
|
100
|
+
if (/^(policy_|guard|guardrail|filter_|moderation_|safety_)/.test(lower)) return 'policy';
|
|
101
|
+
|
|
102
|
+
return 'general';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Split a flat config object into categorised sub-objects.
|
|
107
|
+
*/
|
|
108
|
+
export function splitConfig(
|
|
109
|
+
config: Record<string, unknown>,
|
|
110
|
+
): Record<string, Record<string, unknown>> {
|
|
111
|
+
const buckets: Record<string, Record<string, unknown>> = {};
|
|
112
|
+
for (const [key, value] of Object.entries(config)) {
|
|
113
|
+
const category = classifyConfigKey(key);
|
|
114
|
+
if (!buckets[category]) buckets[category] = {};
|
|
115
|
+
buckets[category][key] = value;
|
|
116
|
+
}
|
|
117
|
+
return buckets;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Configuration for fingerprinting and change detection.
|
|
122
|
+
*/
|
|
123
|
+
export interface FingerprintConfig {
|
|
124
|
+
/** Master switch for fingerprinting. Default: true */
|
|
125
|
+
enabled?: boolean;
|
|
126
|
+
/** Hash prompts for change detection. Default: true */
|
|
127
|
+
capturePrompts?: boolean;
|
|
128
|
+
/** Hash configs for change detection. Default: true */
|
|
129
|
+
captureConfigs?: boolean;
|
|
130
|
+
/** Track DAG structure and edges. Default: true */
|
|
131
|
+
captureStructure?: boolean;
|
|
132
|
+
/** Config keys to redact before hashing. Default: ["apiKey", "password", "secret"] */
|
|
133
|
+
sensitiveConfigKeys?: string[];
|
|
134
|
+
/** Send fingerprint asynchronously. Default: true */
|
|
135
|
+
sendAsync?: boolean;
|
|
136
|
+
/** Send fingerprint even if run fails. Default: true */
|
|
137
|
+
sendOnFailure?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Default fingerprint configuration values.
|
|
142
|
+
*/
|
|
143
|
+
export const DEFAULT_FINGERPRINT_CONFIG: Required<FingerprintConfig> = {
|
|
144
|
+
enabled: true,
|
|
145
|
+
capturePrompts: true,
|
|
146
|
+
captureConfigs: true,
|
|
147
|
+
captureStructure: true,
|
|
148
|
+
sensitiveConfigKeys: ['apiKey', 'password', 'secret', 'token', 'key', 'credential'],
|
|
149
|
+
sendAsync: true,
|
|
150
|
+
sendOnFailure: true,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fingerprint data to be sent to backend.
|
|
155
|
+
*/
|
|
156
|
+
export interface FingerprintData {
|
|
157
|
+
/** Hash of the overall workflow structure */
|
|
158
|
+
structure_hash: string;
|
|
159
|
+
/** Ordered sequence of node names */
|
|
160
|
+
node_sequence: string[];
|
|
161
|
+
/** Count of nodes by type: {"llm": 2, "tool": 1} */
|
|
162
|
+
node_types: Record<string, number>;
|
|
163
|
+
/** Per-node type mapping: {"my_llm_node": "llm", "my_tool_node": "tool"} */
|
|
164
|
+
node_type_mapping: Record<string, string>;
|
|
165
|
+
/** Parent->child edges: ["parent->child", ...] */
|
|
166
|
+
edges: string[];
|
|
167
|
+
/** Hash of each node's full config (backward compat) */
|
|
168
|
+
node_config_hashes: Record<string, string>;
|
|
169
|
+
/** Hash of each LLM node's prompt */
|
|
170
|
+
node_prompt_hashes: Record<string, string>;
|
|
171
|
+
/** Hash of model-identity + generation params per LLM node */
|
|
172
|
+
node_model_hashes: Record<string, string>;
|
|
173
|
+
/** Hash of evaluation/scoring config per node */
|
|
174
|
+
node_eval_config_hashes: Record<string, string>;
|
|
175
|
+
/** Hash of guardrail/safety/policy config per node */
|
|
176
|
+
node_policy_config_hashes: Record<string, string>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Builds workflow fingerprints during execution.
|
|
181
|
+
*
|
|
182
|
+
* This class collects information about nodes, their types, configs, and prompts
|
|
183
|
+
* during workflow execution to create a fingerprint that enables change detection.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const builder = new FingerprintBuilder(config);
|
|
188
|
+
* builder.recordNode("chat_llm", "llm", { model: "gpt-4" }, "You are a helpful assistant");
|
|
189
|
+
* builder.recordNode("search_tool", "tool", { api: "google" });
|
|
190
|
+
* const fingerprint = builder.getFingerprint();
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export class FingerprintBuilder {
|
|
194
|
+
private config: Required<FingerprintConfig>;
|
|
195
|
+
private nodes: string[] = [];
|
|
196
|
+
private nodeTypes: Record<string, string> = {};
|
|
197
|
+
private nodeTypeCounts: Record<string, number> = {};
|
|
198
|
+
private edgeSet: Set<string> = new Set();
|
|
199
|
+
private parentStack: string[] = [];
|
|
200
|
+
private sensitiveKeysLower: Set<string>;
|
|
201
|
+
|
|
202
|
+
// Legacy combined config hash (backward compat)
|
|
203
|
+
private nodeConfigs: Record<string, string> = {};
|
|
204
|
+
private nodePrompts: Record<string, string> = {};
|
|
205
|
+
|
|
206
|
+
// Categorised config hashes
|
|
207
|
+
private nodeModelConfigs: Record<string, string> = {};
|
|
208
|
+
private nodeEvalConfigs: Record<string, string> = {};
|
|
209
|
+
private nodePolicyConfigs: Record<string, string> = {};
|
|
210
|
+
|
|
211
|
+
constructor(config: FingerprintConfig = {}) {
|
|
212
|
+
this.config = { ...DEFAULT_FINGERPRINT_CONFIG, ...config };
|
|
213
|
+
this.sensitiveKeysLower = new Set(
|
|
214
|
+
this.config.sensitiveConfigKeys.map((k) => k.toLowerCase())
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Push a node onto the parent stack for edge tracking.
|
|
220
|
+
*/
|
|
221
|
+
pushParent(nodeName: string): void {
|
|
222
|
+
this.parentStack.push(nodeName);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Pop the current parent from the stack.
|
|
227
|
+
*/
|
|
228
|
+
popParent(): string | undefined {
|
|
229
|
+
return this.parentStack.pop();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get the current parent node (top of stack).
|
|
234
|
+
*/
|
|
235
|
+
currentParent(): string | undefined {
|
|
236
|
+
return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Record a node in the fingerprint.
|
|
241
|
+
*
|
|
242
|
+
* The config object is automatically split into categories (model, eval,
|
|
243
|
+
* policy, general) and hashed separately. The combined hash is also
|
|
244
|
+
* retained for backward compatibility with older backend versions.
|
|
245
|
+
*
|
|
246
|
+
* @param name - Unique name for this node
|
|
247
|
+
* @param nodeType - Type of node (e.g., "llm", "tool", "function", "retriever")
|
|
248
|
+
* @param config - Configuration dict for the node (will be categorised and hashed)
|
|
249
|
+
* @param prompt - System prompt for LLM nodes (will be hashed)
|
|
250
|
+
*/
|
|
251
|
+
recordNode(
|
|
252
|
+
name: string,
|
|
253
|
+
nodeType: string,
|
|
254
|
+
config?: unknown,
|
|
255
|
+
prompt?: string
|
|
256
|
+
): void {
|
|
257
|
+
if (this.nodeTypes[name] !== undefined) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.nodes.push(name);
|
|
262
|
+
this.nodeTypes[name] = nodeType;
|
|
263
|
+
this.nodeTypeCounts[nodeType] = (this.nodeTypeCounts[nodeType] || 0) + 1;
|
|
264
|
+
|
|
265
|
+
// Record edge from parent
|
|
266
|
+
if (this.config.captureStructure) {
|
|
267
|
+
const parent = this.currentParent();
|
|
268
|
+
if (parent) {
|
|
269
|
+
this.edgeSet.add(`${parent}->${name}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Hash and store config (both combined and per-category)
|
|
274
|
+
if (this.config.captureConfigs && config !== undefined) {
|
|
275
|
+
const sanitized = this.sanitizeConfig(config);
|
|
276
|
+
|
|
277
|
+
// Legacy combined hash
|
|
278
|
+
this.nodeConfigs[name] = this.hashValue(sanitized);
|
|
279
|
+
|
|
280
|
+
// Categorised hashes
|
|
281
|
+
if (typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized)) {
|
|
282
|
+
const buckets = splitConfig(sanitized as Record<string, unknown>);
|
|
283
|
+
if (buckets.model) this.nodeModelConfigs[name] = this.hashValue(buckets.model);
|
|
284
|
+
if (buckets.eval) this.nodeEvalConfigs[name] = this.hashValue(buckets.eval);
|
|
285
|
+
if (buckets.policy) this.nodePolicyConfigs[name] = this.hashValue(buckets.policy);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Hash and store prompt
|
|
290
|
+
if (this.config.capturePrompts && prompt !== undefined) {
|
|
291
|
+
this.nodePrompts[name] = this.hashValue(prompt);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Build and return the final fingerprint data.
|
|
297
|
+
*/
|
|
298
|
+
getFingerprint(): FingerprintData {
|
|
299
|
+
const structureData = {
|
|
300
|
+
nodes: this.nodes,
|
|
301
|
+
node_types: this.nodeTypes,
|
|
302
|
+
edges: [...this.edgeSet],
|
|
303
|
+
};
|
|
304
|
+
const structureHash = this.hashValue(structureData);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
structure_hash: structureHash,
|
|
308
|
+
node_sequence: [...this.nodes],
|
|
309
|
+
node_types: { ...this.nodeTypeCounts },
|
|
310
|
+
node_type_mapping: { ...this.nodeTypes },
|
|
311
|
+
edges: [...this.edgeSet],
|
|
312
|
+
node_config_hashes: { ...this.nodeConfigs },
|
|
313
|
+
node_prompt_hashes: { ...this.nodePrompts },
|
|
314
|
+
node_model_hashes: { ...this.nodeModelConfigs },
|
|
315
|
+
node_eval_config_hashes: { ...this.nodeEvalConfigs },
|
|
316
|
+
node_policy_config_hashes: { ...this.nodePolicyConfigs },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Remove sensitive keys from config before hashing.
|
|
322
|
+
*/
|
|
323
|
+
private sanitizeConfig(config: unknown, depth = 0): unknown {
|
|
324
|
+
if (depth > 20 || typeof config !== 'object' || config === null) {
|
|
325
|
+
return config;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (Array.isArray(config)) {
|
|
329
|
+
return config.map((item) => this.sanitizeConfig(item, depth + 1));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const sanitized: Record<string, unknown> = Object.create(null);
|
|
333
|
+
for (const [key, value] of Object.entries(config as Record<string, unknown>)) {
|
|
334
|
+
if (FORBIDDEN_KEYS.has(key)) continue;
|
|
335
|
+
if (this.sensitiveKeysLower.has(key.toLowerCase())) {
|
|
336
|
+
sanitized[key] = '[REDACTED]';
|
|
337
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
338
|
+
sanitized[key] = this.sanitizeConfig(value, depth + 1);
|
|
339
|
+
} else {
|
|
340
|
+
sanitized[key] = value;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return sanitized;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Hash a value using the configured algorithm.
|
|
349
|
+
*/
|
|
350
|
+
private hashValue(value: unknown): string {
|
|
351
|
+
let data: string;
|
|
352
|
+
if (typeof value === 'string') {
|
|
353
|
+
data = value;
|
|
354
|
+
} else {
|
|
355
|
+
try {
|
|
356
|
+
data = JSON.stringify(sortKeysDeep(value));
|
|
357
|
+
} catch {
|
|
358
|
+
data = String(value);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return sha256hex(data).substring(0, 16);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Extract system prompt from a list of chat messages.
|
|
368
|
+
*
|
|
369
|
+
* @param messages - List of message objects with 'role' and 'content' keys
|
|
370
|
+
* @returns The system prompt if found, otherwise undefined
|
|
371
|
+
*/
|
|
372
|
+
export function extractPromptFromMessages(
|
|
373
|
+
messages: Array<{ role?: string; content?: string | unknown[] }>
|
|
374
|
+
): string | undefined {
|
|
375
|
+
if (!messages || !Array.isArray(messages)) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const msg of messages) {
|
|
380
|
+
if (msg.role === 'system') {
|
|
381
|
+
const content = msg.content;
|
|
382
|
+
if (typeof content === 'string') {
|
|
383
|
+
return content;
|
|
384
|
+
} else if (Array.isArray(content)) {
|
|
385
|
+
// Handle structured content
|
|
386
|
+
const parts: string[] = [];
|
|
387
|
+
for (const part of content) {
|
|
388
|
+
if (typeof part === 'string') {
|
|
389
|
+
parts.push(part);
|
|
390
|
+
} else if (typeof part === 'object' && part !== null && 'text' in part) {
|
|
391
|
+
parts.push((part as { text: string }).text);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Extract a subset of config keys from an object.
|
|
404
|
+
*
|
|
405
|
+
* @param obj - Full object
|
|
406
|
+
* @param keys - Keys to extract
|
|
407
|
+
* @returns Object with only the specified keys that exist
|
|
408
|
+
*/
|
|
409
|
+
export function extractConfigSubset(
|
|
410
|
+
obj: Record<string, unknown>,
|
|
411
|
+
keys: string[]
|
|
412
|
+
): Record<string, unknown> {
|
|
413
|
+
const result: Record<string, unknown> = {};
|
|
414
|
+
for (const key of keys) {
|
|
415
|
+
if (key in obj && obj[key] !== undefined) {
|
|
416
|
+
result[key] = obj[key];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { TuringPulseConfig } from './config';
|
|
2
|
+
import { ExecutionContext } from './context';
|
|
3
|
+
import { TuringPulseHttpClient } from './http';
|
|
4
|
+
import { KPIResult, kpiMetadata } from './kpi';
|
|
5
|
+
import { safeErrorMessage } from './utils';
|
|
6
|
+
|
|
7
|
+
export type GovernanceMode = 'hitl' | 'hatl' | 'hotl';
|
|
8
|
+
|
|
9
|
+
export interface GovernanceDirectiveOptions {
|
|
10
|
+
hitl?: boolean;
|
|
11
|
+
hatl?: boolean;
|
|
12
|
+
hotl?: boolean;
|
|
13
|
+
reviewers?: string[];
|
|
14
|
+
escalationChannels?: string[];
|
|
15
|
+
title?: string;
|
|
16
|
+
notes?: string;
|
|
17
|
+
metadata?: Record<string, string>;
|
|
18
|
+
severity?: string;
|
|
19
|
+
/** Escalation timeout — enforced by the backend, not the SDK. */
|
|
20
|
+
autoEscalateAfterSeconds?: number;
|
|
21
|
+
taskPrefix?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class GovernanceDirective {
|
|
25
|
+
readonly hitl: boolean;
|
|
26
|
+
readonly hatl: boolean;
|
|
27
|
+
readonly hotl: boolean;
|
|
28
|
+
readonly reviewers: string[];
|
|
29
|
+
readonly escalationChannels: string[];
|
|
30
|
+
readonly title?: string;
|
|
31
|
+
readonly notes?: string;
|
|
32
|
+
readonly metadata: Record<string, string>;
|
|
33
|
+
readonly severity: string;
|
|
34
|
+
readonly autoEscalateAfterSeconds?: number;
|
|
35
|
+
readonly taskPrefix?: string;
|
|
36
|
+
|
|
37
|
+
constructor(options: GovernanceDirectiveOptions = {}) {
|
|
38
|
+
this.hitl = Boolean(options.hitl);
|
|
39
|
+
this.hatl = Boolean(options.hatl);
|
|
40
|
+
this.hotl = Boolean(options.hotl);
|
|
41
|
+
this.reviewers = options.reviewers ?? [];
|
|
42
|
+
this.escalationChannels = options.escalationChannels ?? [];
|
|
43
|
+
this.title = options.title;
|
|
44
|
+
this.notes = options.notes;
|
|
45
|
+
this.metadata = { ...(options.metadata ?? {}) };
|
|
46
|
+
this.severity = options.severity ?? 'medium';
|
|
47
|
+
this.autoEscalateAfterSeconds = options.autoEscalateAfterSeconds;
|
|
48
|
+
this.taskPrefix = options.taskPrefix;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
modes(): GovernanceMode[] {
|
|
52
|
+
const modes: GovernanceMode[] = [];
|
|
53
|
+
if (this.hitl) modes.push('hitl');
|
|
54
|
+
if (this.hatl) modes.push('hatl');
|
|
55
|
+
if (this.hotl) modes.push('hotl');
|
|
56
|
+
return modes;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface GovernanceTaskResult {
|
|
61
|
+
mode: GovernanceMode;
|
|
62
|
+
taskId?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class GovernanceManager {
|
|
66
|
+
constructor(private readonly client: TuringPulseHttpClient, private readonly config: TuringPulseConfig) {}
|
|
67
|
+
|
|
68
|
+
async enforce(
|
|
69
|
+
directive: GovernanceDirective | undefined,
|
|
70
|
+
context: ExecutionContext,
|
|
71
|
+
kpiResults: KPIResult[],
|
|
72
|
+
): Promise<GovernanceTaskResult[]> {
|
|
73
|
+
if (!directive) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const metadata = this.buildMetadata(directive, context, kpiResults);
|
|
77
|
+
const tasks: GovernanceTaskResult[] = [];
|
|
78
|
+
for (const mode of directive.modes()) {
|
|
79
|
+
const payload = this.buildTaskPayload(mode, directive, context, metadata);
|
|
80
|
+
try {
|
|
81
|
+
const taskId = await this.client.createTask(payload);
|
|
82
|
+
tasks.push({ mode, taskId });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.warn(`Failed to create governance task for ${mode}:`, safeErrorMessage(error));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return tasks;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private buildMetadata(
|
|
92
|
+
directive: GovernanceDirective,
|
|
93
|
+
context: ExecutionContext,
|
|
94
|
+
kpiResults: KPIResult[],
|
|
95
|
+
): Record<string, string> {
|
|
96
|
+
const metadata: Record<string, string> = {
|
|
97
|
+
...this.config.governanceDefaults.metadata,
|
|
98
|
+
...directive.metadata,
|
|
99
|
+
severity: directive.severity || this.config.governanceDefaults.severity,
|
|
100
|
+
'agent.run_id': context.runId,
|
|
101
|
+
'agent.operation': context.operation,
|
|
102
|
+
'agent.hidden_entrypoint': String(context.hiddenEntrypoint),
|
|
103
|
+
};
|
|
104
|
+
for (const result of kpiResults) {
|
|
105
|
+
Object.assign(metadata, kpiMetadata(result));
|
|
106
|
+
}
|
|
107
|
+
return metadata;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private buildTaskPayload(
|
|
111
|
+
mode: GovernanceMode,
|
|
112
|
+
directive: GovernanceDirective,
|
|
113
|
+
context: ExecutionContext,
|
|
114
|
+
metadata: Record<string, string>,
|
|
115
|
+
): Record<string, unknown> {
|
|
116
|
+
const title = directive.title ?? `${mode.toUpperCase()} review for ${context.operation}`;
|
|
117
|
+
const taskIdPrefix = directive.taskPrefix ?? context.runId;
|
|
118
|
+
const reviewers = directive.reviewers.length ? directive.reviewers : this.config.governanceDefaults.reviewers;
|
|
119
|
+
const channels = directive.escalationChannels.length
|
|
120
|
+
? directive.escalationChannels
|
|
121
|
+
: this.config.governanceDefaults.escalationChannels;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
taskId: `${taskIdPrefix}-${mode}`,
|
|
125
|
+
title,
|
|
126
|
+
status: defaultTaskStatus(mode),
|
|
127
|
+
agentRunId: context.runId,
|
|
128
|
+
hitlRequired: mode === 'hitl',
|
|
129
|
+
metadata: {
|
|
130
|
+
...metadata,
|
|
131
|
+
'governance.mode': mode,
|
|
132
|
+
'governance.reviewers': reviewers.join(','),
|
|
133
|
+
'governance.channels': channels.join(','),
|
|
134
|
+
...(directive.notes ? { 'governance.notes': directive.notes } : {}),
|
|
135
|
+
...((directive.autoEscalateAfterSeconds ?? this.config.governanceDefaults.autoEscalateAfterSeconds) != null
|
|
136
|
+
? {
|
|
137
|
+
'governance.auto_escalate_after_seconds': String(
|
|
138
|
+
directive.autoEscalateAfterSeconds ?? this.config.governanceDefaults.autoEscalateAfterSeconds,
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function defaultTaskStatus(mode: GovernanceMode): string {
|
|
148
|
+
switch (mode) {
|
|
149
|
+
case 'hitl':
|
|
150
|
+
return 'needs_human';
|
|
151
|
+
case 'hotl':
|
|
152
|
+
return 'in_progress';
|
|
153
|
+
default:
|
|
154
|
+
return 'todo';
|
|
155
|
+
}
|
|
156
|
+
}
|