@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.
Files changed (160) hide show
  1. package/.github/dependabot.yml +38 -0
  2. package/.github/workflows/ci.yml +246 -0
  3. package/.github/workflows/framework-compat.yml +169 -0
  4. package/.github/workflows/security.yml +336 -0
  5. package/CHANGELOG.md +29 -0
  6. package/LICENSE +13 -0
  7. package/MIGRATION.md +30 -0
  8. package/README.md +221 -0
  9. package/dist/attachments.d.ts +28 -0
  10. package/dist/attachments.d.ts.map +1 -0
  11. package/dist/attachments.js +59 -0
  12. package/dist/attachments.js.map +1 -0
  13. package/dist/config.d.ts +72 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +78 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/context.d.ts +126 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +163 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/decorators.d.ts +6 -0
  22. package/dist/decorators.d.ts.map +1 -0
  23. package/dist/decorators.js +52 -0
  24. package/dist/decorators.js.map +1 -0
  25. package/dist/deploy.d.ts +89 -0
  26. package/dist/deploy.d.ts.map +1 -0
  27. package/dist/deploy.js +203 -0
  28. package/dist/deploy.js.map +1 -0
  29. package/dist/errors.d.ts +18 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +34 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/eventBuilder.d.ts +21 -0
  34. package/dist/eventBuilder.d.ts.map +1 -0
  35. package/dist/eventBuilder.js +127 -0
  36. package/dist/eventBuilder.js.map +1 -0
  37. package/dist/fingerprint.d.ts +158 -0
  38. package/dist/fingerprint.d.ts.map +1 -0
  39. package/dist/fingerprint.js +339 -0
  40. package/dist/fingerprint.js.map +1 -0
  41. package/dist/governance.d.ts +47 -0
  42. package/dist/governance.d.ts.map +1 -0
  43. package/dist/governance.js +104 -0
  44. package/dist/governance.js.map +1 -0
  45. package/dist/http.d.ts +62 -0
  46. package/dist/http.d.ts.map +1 -0
  47. package/dist/http.js +181 -0
  48. package/dist/http.js.map +1 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +23 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/instrumentation.d.ts +40 -0
  54. package/dist/instrumentation.d.ts.map +1 -0
  55. package/dist/instrumentation.js +31 -0
  56. package/dist/instrumentation.js.map +1 -0
  57. package/dist/integrations/mastra.d.ts +64 -0
  58. package/dist/integrations/mastra.d.ts.map +1 -0
  59. package/dist/integrations/mastra.js +256 -0
  60. package/dist/integrations/mastra.js.map +1 -0
  61. package/dist/kpi.d.ts +21 -0
  62. package/dist/kpi.d.ts.map +1 -0
  63. package/dist/kpi.js +83 -0
  64. package/dist/kpi.js.map +1 -0
  65. package/dist/llmDetector.d.ts +22 -0
  66. package/dist/llmDetector.d.ts.map +1 -0
  67. package/dist/llmDetector.js +269 -0
  68. package/dist/llmDetector.js.map +1 -0
  69. package/dist/plugin.d.ts +33 -0
  70. package/dist/plugin.d.ts.map +1 -0
  71. package/dist/plugin.js +312 -0
  72. package/dist/plugin.js.map +1 -0
  73. package/dist/registry.d.ts +13 -0
  74. package/dist/registry.d.ts.map +1 -0
  75. package/dist/registry.js +18 -0
  76. package/dist/registry.js.map +1 -0
  77. package/dist/tracing.d.ts +10 -0
  78. package/dist/tracing.d.ts.map +1 -0
  79. package/dist/tracing.js +30 -0
  80. package/dist/tracing.js.map +1 -0
  81. package/dist/triggerState.d.ts +5 -0
  82. package/dist/triggerState.d.ts.map +1 -0
  83. package/dist/triggerState.js +19 -0
  84. package/dist/triggerState.js.map +1 -0
  85. package/dist/utils.d.ts +27 -0
  86. package/dist/utils.d.ts.map +1 -0
  87. package/dist/utils.js +72 -0
  88. package/dist/utils.js.map +1 -0
  89. package/package.json +37 -0
  90. package/packages/anthropic/package.json +16 -0
  91. package/packages/anthropic/src/index.ts +5 -0
  92. package/packages/anthropic/src/wrapper.ts +102 -0
  93. package/packages/anthropic/tsconfig.build.json +20 -0
  94. package/packages/langchain/package.json +16 -0
  95. package/packages/langchain/src/index.ts +7 -0
  96. package/packages/langchain/src/wrapper.ts +51 -0
  97. package/packages/mastra/package.json +17 -0
  98. package/packages/mastra/src/index.ts +8 -0
  99. package/packages/mastra/src/wrapper.ts +301 -0
  100. package/packages/openai/package.json +16 -0
  101. package/packages/openai/src/index.ts +8 -0
  102. package/packages/openai/src/wrapper.ts +103 -0
  103. package/packages/openai/tsconfig.build.json +20 -0
  104. package/packages/openclaw/openclaw.plugin.json +100 -0
  105. package/packages/openclaw/package.json +41 -0
  106. package/packages/openclaw/src/buffer.ts +99 -0
  107. package/packages/openclaw/src/config.ts +139 -0
  108. package/packages/openclaw/src/hooks/governance.ts +267 -0
  109. package/packages/openclaw/src/hooks/lifecycle.ts +75 -0
  110. package/packages/openclaw/src/hooks/telemetry.ts +207 -0
  111. package/packages/openclaw/src/index.ts +91 -0
  112. package/packages/openclaw/src/mapper.ts +233 -0
  113. package/packages/openclaw/src/session-tracker.ts +181 -0
  114. package/packages/openclaw/src/types.ts +220 -0
  115. package/packages/openclaw/tests/buffer.test.ts +148 -0
  116. package/packages/openclaw/tests/config.test.ts +122 -0
  117. package/packages/openclaw/tests/governance.test.ts +232 -0
  118. package/packages/openclaw/tests/mapper.test.ts +242 -0
  119. package/packages/openclaw/tests/session-tracker.test.ts +124 -0
  120. package/packages/openclaw/tsconfig.json +18 -0
  121. package/packages/openclaw/vitest.config.ts +8 -0
  122. package/packages/vercel-ai/package.json +16 -0
  123. package/packages/vercel-ai/src/index.ts +5 -0
  124. package/packages/vercel-ai/src/wrapper.ts +49 -0
  125. package/scripts/bump-version.sh +58 -0
  126. package/scripts/update-readme-compat.mjs +151 -0
  127. package/src/__tests__/fingerprint.test.ts +328 -0
  128. package/src/attachments.ts +88 -0
  129. package/src/config.ts +164 -0
  130. package/src/context.ts +258 -0
  131. package/src/decorators.ts +61 -0
  132. package/src/deploy.ts +260 -0
  133. package/src/errors.ts +44 -0
  134. package/src/eventBuilder.ts +153 -0
  135. package/src/fingerprint.ts +421 -0
  136. package/src/governance.ts +156 -0
  137. package/src/http.ts +241 -0
  138. package/src/index.ts +57 -0
  139. package/src/instrumentation.ts +68 -0
  140. package/src/integrations/mastra.ts +335 -0
  141. package/src/kpi.ts +112 -0
  142. package/src/llmDetector.ts +330 -0
  143. package/src/plugin.ts +384 -0
  144. package/src/registry.ts +27 -0
  145. package/src/tracing.ts +39 -0
  146. package/src/triggerState.ts +27 -0
  147. package/src/utils.ts +78 -0
  148. package/tests/compat/anthropic.test.ts +61 -0
  149. package/tests/compat/cohere.test.ts +57 -0
  150. package/tests/compat/google-genai.test.ts +61 -0
  151. package/tests/compat/langchain-openai.test.ts +41 -0
  152. package/tests/compat/langchain.test.ts +64 -0
  153. package/tests/compat/mistral.test.ts +58 -0
  154. package/tests/compat/openai.test.ts +71 -0
  155. package/tests/compat/vercel-ai.test.ts +56 -0
  156. package/tests/plugins/anthropic-wrapper.test.ts +120 -0
  157. package/tests/plugins/langchain-wrapper.test.ts +128 -0
  158. package/tests/plugins/openai-wrapper.test.ts +165 -0
  159. package/tsconfig.json +21 -0
  160. package/vitest.config.ts +9 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Plugin configuration types and defaults.
3
+ *
4
+ * The JSON Schema in openclaw.plugin.json validates at config read/write
5
+ * time. This module provides runtime types and default resolution.
6
+ */
7
+
8
+ export interface GovernanceConfig {
9
+ enabled: boolean;
10
+ failMode: 'open' | 'closed';
11
+ timeoutMs: number;
12
+ excludeTools: string[];
13
+ scanOutboundMessages: boolean;
14
+ }
15
+
16
+ export interface TelemetryConfig {
17
+ enabled: boolean;
18
+ batchSize: number;
19
+ flushIntervalMs: number;
20
+ captureMessageContent: boolean;
21
+ captureToolParams: boolean;
22
+ redactPatterns: string[];
23
+ }
24
+
25
+ export interface TuringPulseOpenClawConfig {
26
+ apiKey: string;
27
+ endpoint: string;
28
+ governance: GovernanceConfig;
29
+ telemetry: TelemetryConfig;
30
+ metadata: Record<string, string>;
31
+ }
32
+
33
+ const DEFAULT_GOVERNANCE: GovernanceConfig = {
34
+ enabled: true,
35
+ failMode: 'open',
36
+ timeoutMs: 3000,
37
+ excludeTools: [],
38
+ scanOutboundMessages: false,
39
+ };
40
+
41
+ const DEFAULT_TELEMETRY: TelemetryConfig = {
42
+ enabled: true,
43
+ batchSize: 20,
44
+ flushIntervalMs: 5000,
45
+ captureMessageContent: false,
46
+ captureToolParams: true,
47
+ redactPatterns: [],
48
+ };
49
+
50
+ const DEFAULT_ENDPOINT = 'https://api.turingpulse.ai';
51
+
52
+ /**
53
+ * Resolve raw plugin config (from openclaw.json) into a fully typed
54
+ * config object with all defaults applied.
55
+ */
56
+ export function resolveConfig(
57
+ raw: Record<string, unknown>,
58
+ ): TuringPulseOpenClawConfig {
59
+ const apiKey = raw.apiKey;
60
+ if (typeof apiKey !== 'string' || apiKey.trim().length === 0) {
61
+ throw new Error(
62
+ 'TuringPulse plugin: "apiKey" is required in plugin configuration. ' +
63
+ 'Get an API key from your TuringPulse project settings.',
64
+ );
65
+ }
66
+
67
+ const endpoint =
68
+ typeof raw.endpoint === 'string' && raw.endpoint.trim().length > 0
69
+ ? raw.endpoint.trim().replace(/\/+$/, '')
70
+ : DEFAULT_ENDPOINT;
71
+
72
+ const rawGov = (raw.governance ?? {}) as Record<string, unknown>;
73
+ const governance: GovernanceConfig = {
74
+ enabled: typeof rawGov.enabled === 'boolean' ? rawGov.enabled : DEFAULT_GOVERNANCE.enabled,
75
+ failMode: rawGov.failMode === 'closed' ? 'closed' : DEFAULT_GOVERNANCE.failMode,
76
+ timeoutMs:
77
+ typeof rawGov.timeoutMs === 'number' && rawGov.timeoutMs > 0
78
+ ? rawGov.timeoutMs
79
+ : DEFAULT_GOVERNANCE.timeoutMs,
80
+ excludeTools: Array.isArray(rawGov.excludeTools)
81
+ ? (rawGov.excludeTools as unknown[]).filter((t): t is string => typeof t === 'string')
82
+ : DEFAULT_GOVERNANCE.excludeTools,
83
+ scanOutboundMessages:
84
+ typeof rawGov.scanOutboundMessages === 'boolean'
85
+ ? rawGov.scanOutboundMessages
86
+ : DEFAULT_GOVERNANCE.scanOutboundMessages,
87
+ };
88
+
89
+ const rawTel = (raw.telemetry ?? {}) as Record<string, unknown>;
90
+ const telemetry: TelemetryConfig = {
91
+ enabled: typeof rawTel.enabled === 'boolean' ? rawTel.enabled : DEFAULT_TELEMETRY.enabled,
92
+ batchSize:
93
+ typeof rawTel.batchSize === 'number' && rawTel.batchSize > 0
94
+ ? rawTel.batchSize
95
+ : DEFAULT_TELEMETRY.batchSize,
96
+ flushIntervalMs:
97
+ typeof rawTel.flushIntervalMs === 'number' && rawTel.flushIntervalMs > 0
98
+ ? rawTel.flushIntervalMs
99
+ : DEFAULT_TELEMETRY.flushIntervalMs,
100
+ captureMessageContent:
101
+ typeof rawTel.captureMessageContent === 'boolean'
102
+ ? rawTel.captureMessageContent
103
+ : DEFAULT_TELEMETRY.captureMessageContent,
104
+ captureToolParams:
105
+ typeof rawTel.captureToolParams === 'boolean'
106
+ ? rawTel.captureToolParams
107
+ : DEFAULT_TELEMETRY.captureToolParams,
108
+ redactPatterns: Array.isArray(rawTel.redactPatterns)
109
+ ? (rawTel.redactPatterns as unknown[]).filter((p): p is string => typeof p === 'string')
110
+ : DEFAULT_TELEMETRY.redactPatterns,
111
+ };
112
+
113
+ const rawMeta = (raw.metadata ?? {}) as Record<string, unknown>;
114
+ const metadata: Record<string, string> = {};
115
+ for (const [k, v] of Object.entries(rawMeta)) {
116
+ if (typeof v === 'string') {
117
+ metadata[k] = v;
118
+ }
119
+ }
120
+
121
+ return { apiKey: apiKey.trim(), endpoint, governance, telemetry, metadata };
122
+ }
123
+
124
+ /**
125
+ * Compile redact patterns into a single RegExp for efficient scanning.
126
+ * Returns null if no patterns are configured.
127
+ */
128
+ export function compileRedactPatterns(patterns: string[]): RegExp | null {
129
+ if (patterns.length === 0) return null;
130
+
131
+ const DEFAULT_PII_PATTERNS = [
132
+ '\\b\\d{3}-\\d{2}-\\d{4}\\b',
133
+ '\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b',
134
+ '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b',
135
+ ];
136
+
137
+ const allPatterns = [...DEFAULT_PII_PATTERNS, ...patterns];
138
+ return new RegExp(`(${allPatterns.join('|')})`, 'g');
139
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Deterministic governance hook for OpenClaw tool execution.
3
+ *
4
+ * Intercepts tool.pre events and calls TuringPulse's policy check API
5
+ * to enforce governance rules OUTSIDE the LLM's decision loop.
6
+ *
7
+ * The LLM decides what tool to call → this hook decides whether to allow it.
8
+ * Policy evaluation is deterministic: if the policy says block, the tool
9
+ * is cancelled regardless of the LLM's intent.
10
+ *
11
+ * Supports:
12
+ * - fail-open / fail-closed modes for TuringPulse unavailability
13
+ * - configurable timeout to avoid blocking the agent loop
14
+ * - tool exclusion lists for trusted/safe tools
15
+ * - PII scanning on outbound messages (message.pre)
16
+ */
17
+
18
+ import type { TuringPulseHttpClient, PolicyCheckParams } from '@turingpulse/sdk';
19
+ import type { GovernanceConfig, TuringPulseOpenClawConfig } from '../config.js';
20
+ import type {
21
+ ToolPreContext,
22
+ ToolPreResult,
23
+ MessageSendingContext,
24
+ MessageSendingResult,
25
+ OpenClawPluginAPI,
26
+ } from '../types.js';
27
+ import type { SessionTracker } from '../session-tracker.js';
28
+
29
+ /**
30
+ * Register the governance hooks on the OpenClaw plugin API.
31
+ */
32
+ export function registerGovernanceHooks(
33
+ api: OpenClawPluginAPI,
34
+ client: TuringPulseHttpClient,
35
+ config: TuringPulseOpenClawConfig,
36
+ sessionTracker: SessionTracker,
37
+ ): void {
38
+ if (!config.governance.enabled) return;
39
+
40
+ api.lifecycle.on(
41
+ 'tool.pre',
42
+ async (rawCtx: unknown): Promise<ToolPreResult | undefined> => {
43
+ const ctx = rawCtx as ToolPreContext;
44
+ return handleToolPre(ctx, client, config, sessionTracker);
45
+ },
46
+ {
47
+ priority: 1000,
48
+ timeoutMs: config.governance.timeoutMs,
49
+ mode: config.governance.failMode === 'closed' ? 'fail-closed' : 'fail-open',
50
+ onTimeout: config.governance.failMode === 'closed' ? 'fail-closed' : 'fail-open',
51
+ },
52
+ );
53
+
54
+ if (config.governance.scanOutboundMessages) {
55
+ api.lifecycle.on(
56
+ 'message.pre',
57
+ async (rawCtx: unknown): Promise<MessageSendingResult | undefined> => {
58
+ const ctx = rawCtx as MessageSendingContext;
59
+ return handleMessagePre(ctx, client, config, sessionTracker);
60
+ },
61
+ {
62
+ priority: 900,
63
+ timeoutMs: config.governance.timeoutMs,
64
+ mode: 'fail-open',
65
+ },
66
+ );
67
+ }
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Tool pre-execution governance check
72
+ // ---------------------------------------------------------------------------
73
+
74
+ async function handleToolPre(
75
+ ctx: ToolPreContext,
76
+ client: TuringPulseHttpClient,
77
+ config: TuringPulseOpenClawConfig,
78
+ sessionTracker: SessionTracker,
79
+ ): Promise<ToolPreResult | undefined> {
80
+ if (config.governance.excludeTools.includes(ctx.toolName)) {
81
+ return undefined;
82
+ }
83
+
84
+ const session = ctx.sessionKey
85
+ ? sessionTracker.get(ctx.sessionKey)
86
+ : undefined;
87
+
88
+ const params: PolicyCheckParams = {
89
+ agentId: ctx.agentId ?? session?.agentId,
90
+ workflowId: ctx.agentId ?? session?.agentId,
91
+ runId: session?.traceId,
92
+ nodeName: ctx.toolName,
93
+ nodeType: 'tool',
94
+ metadata: {
95
+ tool_name: ctx.toolName,
96
+ tool_params: summarizeToolParams(ctx.toolParams),
97
+ channel: ctx.channelId ?? session?.channelId,
98
+ session_key: ctx.sessionKey,
99
+ framework: 'openclaw',
100
+ ...config.metadata,
101
+ },
102
+ };
103
+
104
+ if (session) {
105
+ params.costUsd = session.costUsd;
106
+ }
107
+
108
+ const inputText = buildInputSummary(ctx);
109
+ if (inputText) {
110
+ params.inputText = inputText;
111
+ }
112
+
113
+ try {
114
+ const result = await withTimeout(
115
+ client.policyCheck(params),
116
+ config.governance.timeoutMs,
117
+ );
118
+
119
+ if (result.action === 'block') {
120
+ logGovernanceDecision('BLOCKED', ctx.toolName, result.reason, result.policyIds);
121
+ return { cancel: true };
122
+ }
123
+
124
+ if (result.action === 'flag') {
125
+ logGovernanceDecision('FLAGGED', ctx.toolName, result.reason, result.policyIds);
126
+ }
127
+
128
+ return undefined;
129
+ } catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ // eslint-disable-next-line no-console
132
+ console.warn(`[turingpulse] Governance check failed for tool "${ctx.toolName}": ${message}`);
133
+
134
+ if (config.governance.failMode === 'closed') {
135
+ logGovernanceDecision('BLOCKED (fail-closed)', ctx.toolName, `check_error: ${message}`);
136
+ return { cancel: true };
137
+ }
138
+
139
+ return undefined;
140
+ }
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Outbound message PII scanning
145
+ // ---------------------------------------------------------------------------
146
+
147
+ async function handleMessagePre(
148
+ ctx: MessageSendingContext,
149
+ client: TuringPulseHttpClient,
150
+ config: TuringPulseOpenClawConfig,
151
+ sessionTracker: SessionTracker,
152
+ ): Promise<MessageSendingResult | undefined> {
153
+ const content = ctx.context?.content;
154
+ if (!content || content.length === 0) return undefined;
155
+
156
+ const session = ctx.sessionKey
157
+ ? sessionTracker.get(ctx.sessionKey)
158
+ : undefined;
159
+
160
+ const params: PolicyCheckParams = {
161
+ agentId: session?.agentId,
162
+ runId: session?.traceId,
163
+ nodeName: 'outbound_message',
164
+ nodeType: 'message',
165
+ outputText: content.slice(0, 5000),
166
+ outputLength: content.length,
167
+ metadata: {
168
+ channel: ctx.context?.channelId,
169
+ recipient: ctx.context?.to,
170
+ framework: 'openclaw',
171
+ ...config.metadata,
172
+ },
173
+ };
174
+
175
+ try {
176
+ const result = await withTimeout(
177
+ client.policyCheck(params),
178
+ config.governance.timeoutMs,
179
+ );
180
+
181
+ if (result.action === 'block') {
182
+ logGovernanceDecision('BLOCKED outbound message', 'message_send', result.reason, result.policyIds);
183
+ return { cancel: true };
184
+ }
185
+
186
+ return undefined;
187
+ } catch {
188
+ return undefined;
189
+ }
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Helpers
194
+ // ---------------------------------------------------------------------------
195
+
196
+ /**
197
+ * Summarize tool params for policy evaluation without sending full payloads.
198
+ * Extracts key identifiers like command strings, file paths, URLs.
199
+ */
200
+ function summarizeToolParams(params: Record<string, unknown>): string {
201
+ const parts: string[] = [];
202
+
203
+ for (const [key, value] of Object.entries(params)) {
204
+ if (typeof value === 'string' && value.length > 0) {
205
+ const truncated = value.length > 500 ? value.slice(0, 500) + '...' : value;
206
+ parts.push(`${key}=${truncated}`);
207
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
208
+ parts.push(`${key}=${String(value)}`);
209
+ }
210
+ }
211
+
212
+ return parts.join('; ');
213
+ }
214
+
215
+ /**
216
+ * Build a human-readable input summary for policy condition matching
217
+ * (e.g., keyword detection, PII scanning).
218
+ */
219
+ function buildInputSummary(ctx: ToolPreContext): string | undefined {
220
+ const command =
221
+ ctx.toolParams.command ??
222
+ ctx.toolParams.cmd ??
223
+ ctx.toolParams.script ??
224
+ ctx.toolParams.url ??
225
+ ctx.toolParams.path;
226
+
227
+ if (typeof command === 'string') {
228
+ return `${ctx.toolName}: ${command.slice(0, 2000)}`;
229
+ }
230
+
231
+ return `${ctx.toolName}: ${JSON.stringify(ctx.toolParams).slice(0, 2000)}`;
232
+ }
233
+
234
+ function logGovernanceDecision(
235
+ action: string,
236
+ toolName: string,
237
+ reason?: string,
238
+ policyIds?: string[],
239
+ ): void {
240
+ const parts = [`[turingpulse] ${action}: tool="${toolName}"`];
241
+ if (reason) parts.push(`reason="${reason}"`);
242
+ if (policyIds && policyIds.length > 0) parts.push(`policies=[${policyIds.join(', ')}]`);
243
+ // eslint-disable-next-line no-console
244
+ console.log(parts.join(' '));
245
+ }
246
+
247
+ /**
248
+ * Race a promise against a timeout. Rejects with a timeout error
249
+ * if the promise doesn't resolve in time.
250
+ */
251
+ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
252
+ return new Promise<T>((resolve, reject) => {
253
+ const timer = setTimeout(() => {
254
+ reject(new Error(`Governance check timed out after ${ms}ms`));
255
+ }, ms);
256
+
257
+ promise
258
+ .then((value) => {
259
+ clearTimeout(timer);
260
+ resolve(value);
261
+ })
262
+ .catch((err) => {
263
+ clearTimeout(timer);
264
+ reject(err);
265
+ });
266
+ });
267
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Gateway lifecycle hooks for plugin initialization and shutdown.
3
+ *
4
+ * - boot.post: start the event buffer flush timer
5
+ * - shutdown.post: flush remaining events and clean up
6
+ *
7
+ * Also registers a /turingpulse status command for health reporting.
8
+ */
9
+
10
+ import type { EventBuffer } from '../buffer.js';
11
+ import type { SessionTracker } from '../session-tracker.js';
12
+ import type { TuringPulseOpenClawConfig } from '../config.js';
13
+ import type { OpenClawPluginAPI } from '../types.js';
14
+
15
+ /**
16
+ * Register gateway lifecycle hooks and status command.
17
+ */
18
+ export function registerLifecycleHooks(
19
+ api: OpenClawPluginAPI,
20
+ config: TuringPulseOpenClawConfig,
21
+ buffer: EventBuffer,
22
+ sessionTracker: SessionTracker,
23
+ ): void {
24
+ api.lifecycle.on(
25
+ 'boot.post',
26
+ async () => {
27
+ buffer.start();
28
+ // eslint-disable-next-line no-console
29
+ console.log(
30
+ `[turingpulse] Plugin initialized — ` +
31
+ `governance=${config.governance.enabled ? 'ON' : 'OFF'}, ` +
32
+ `telemetry=${config.telemetry.enabled ? 'ON' : 'OFF'}, ` +
33
+ `endpoint=${config.endpoint}`,
34
+ );
35
+ },
36
+ { priority: 100, mode: 'fail-open' },
37
+ );
38
+
39
+ api.lifecycle.on(
40
+ 'shutdown.post',
41
+ async () => {
42
+ // eslint-disable-next-line no-console
43
+ console.log('[turingpulse] Shutting down — flushing remaining events...');
44
+ await buffer.stop();
45
+ // eslint-disable-next-line no-console
46
+ console.log('[turingpulse] Shutdown complete.');
47
+ },
48
+ { priority: 100, mode: 'fail-open' },
49
+ );
50
+
51
+ api.registerCommand({
52
+ name: 'turingpulse',
53
+ description: 'Show TuringPulse plugin status',
54
+ handler: () => {
55
+ const lines = [
56
+ '--- TuringPulse Plugin Status ---',
57
+ `Endpoint: ${config.endpoint}`,
58
+ `Governance: ${config.governance.enabled ? 'ENABLED' : 'DISABLED'}`,
59
+ ` Fail mode: ${config.governance.failMode}`,
60
+ ` Timeout: ${config.governance.timeoutMs}ms`,
61
+ ` Excluded tools: ${config.governance.excludeTools.length > 0 ? config.governance.excludeTools.join(', ') : 'none'}`,
62
+ ` Outbound PII scan: ${config.governance.scanOutboundMessages ? 'ON' : 'OFF'}`,
63
+ `Telemetry: ${config.telemetry.enabled ? 'ENABLED' : 'DISABLED'}`,
64
+ ` Batch size: ${config.telemetry.batchSize}`,
65
+ ` Flush interval: ${config.telemetry.flushIntervalMs}ms`,
66
+ ` Capture message content: ${config.telemetry.captureMessageContent ? 'ON' : 'OFF'}`,
67
+ ` Redact patterns: ${config.telemetry.redactPatterns.length}`,
68
+ `Active sessions: ${sessionTracker.size}`,
69
+ `Pending events: ${buffer.pending}`,
70
+ `Static metadata: ${JSON.stringify(config.metadata)}`,
71
+ ];
72
+ return { text: lines.join('\n') };
73
+ },
74
+ });
75
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Telemetry hooks that capture OpenClaw lifecycle events as TuringPulse
3
+ * AgentEvent objects.
4
+ *
5
+ * Hook flow per interaction:
6
+ * message_received → start trace (root span + session)
7
+ * agent.pre → mark agent turn start
8
+ * tool.post → capture tool execution as child span
9
+ * agent.post → capture LLM reasoning as child span
10
+ * message_sent → finalize root span, flush all events
11
+ */
12
+
13
+ import { generateUUID } from '@turingpulse/sdk';
14
+ import type { TuringPulseOpenClawConfig } from '../config.js';
15
+ import type { EventBuffer } from '../buffer.js';
16
+ import type { SessionTracker } from '../session-tracker.js';
17
+ import {
18
+ buildRootSpan,
19
+ buildToolSpan,
20
+ buildAgentReasoningSpan,
21
+ } from '../mapper.js';
22
+ import type {
23
+ OpenClawPluginAPI,
24
+ MessageReceivedContext,
25
+ MessageSentContext,
26
+ ToolPostContext,
27
+ AgentPreContext,
28
+ AgentPostContext,
29
+ } from '../types.js';
30
+
31
+ /**
32
+ * Register all telemetry lifecycle hooks on the OpenClaw plugin API.
33
+ */
34
+ export function registerTelemetryHooks(
35
+ api: OpenClawPluginAPI,
36
+ config: TuringPulseOpenClawConfig,
37
+ buffer: EventBuffer,
38
+ sessionTracker: SessionTracker,
39
+ ): void {
40
+ if (!config.telemetry.enabled) return;
41
+
42
+ // ── message_received: start a new trace ──
43
+ api.lifecycle.on(
44
+ 'request.pre',
45
+ async (rawCtx: unknown) => {
46
+ const ctx = rawCtx as MessageReceivedContext;
47
+ onMessageReceived(ctx, sessionTracker);
48
+ },
49
+ { priority: 500, mode: 'fail-open' },
50
+ );
51
+
52
+ // ── agent.pre: mark agent turn start ──
53
+ api.lifecycle.on(
54
+ 'agent.pre',
55
+ async (rawCtx: unknown) => {
56
+ const ctx = rawCtx as AgentPreContext;
57
+ onAgentPre(ctx, sessionTracker);
58
+ },
59
+ { priority: 500, mode: 'fail-open' },
60
+ );
61
+
62
+ // ── tool.post: capture tool execution ──
63
+ api.lifecycle.on(
64
+ 'tool.post',
65
+ async (rawCtx: unknown) => {
66
+ const ctx = rawCtx as ToolPostContext;
67
+ onToolPost(ctx, config, sessionTracker);
68
+ },
69
+ { priority: 500, mode: 'fail-open' },
70
+ );
71
+
72
+ // ── agent.post: capture LLM reasoning ──
73
+ api.lifecycle.on(
74
+ 'agent.post',
75
+ async (rawCtx: unknown) => {
76
+ const ctx = rawCtx as AgentPostContext;
77
+ onAgentPost(ctx, config, sessionTracker);
78
+ },
79
+ { priority: 500, mode: 'fail-open' },
80
+ );
81
+
82
+ // ── message_sent: finalize and flush ──
83
+ api.lifecycle.on(
84
+ 'message.post',
85
+ async (rawCtx: unknown) => {
86
+ const ctx = rawCtx as MessageSentContext;
87
+ await onMessageSent(ctx, config, buffer, sessionTracker);
88
+ },
89
+ { priority: 500, mode: 'fail-open' },
90
+ );
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Hook handlers
95
+ // ---------------------------------------------------------------------------
96
+
97
+ function onMessageReceived(
98
+ ctx: MessageReceivedContext,
99
+ sessionTracker: SessionTracker,
100
+ ): void {
101
+ const sessionKey = ctx.sessionKey;
102
+ if (!sessionKey) return;
103
+
104
+ const traceId = generateUUID();
105
+ const rootSpanId = generateUUID();
106
+
107
+ sessionTracker.start(
108
+ sessionKey,
109
+ traceId,
110
+ rootSpanId,
111
+ ctx.context?.channelId,
112
+ undefined,
113
+ );
114
+ }
115
+
116
+ function onAgentPre(
117
+ ctx: AgentPreContext,
118
+ sessionTracker: SessionTracker,
119
+ ): void {
120
+ const sessionKey = ctx.sessionKey;
121
+ if (!sessionKey) return;
122
+
123
+ const session = sessionTracker.get(sessionKey);
124
+ if (!session) return;
125
+
126
+ if (ctx.agentId && !session.agentId) {
127
+ session.agentId = ctx.agentId;
128
+ }
129
+ }
130
+
131
+ function onToolPost(
132
+ ctx: ToolPostContext,
133
+ config: TuringPulseOpenClawConfig,
134
+ sessionTracker: SessionTracker,
135
+ ): void {
136
+ const sessionKey = ctx.sessionKey;
137
+ if (!sessionKey) return;
138
+
139
+ const session = sessionTracker.get(sessionKey);
140
+ if (!session) return;
141
+
142
+ const spanId = generateUUID();
143
+ const event = buildToolSpan(ctx, session, spanId, config);
144
+
145
+ sessionTracker.addChildEvent(sessionKey, event);
146
+ sessionTracker.addMetrics(sessionKey, { toolCalls: 1 });
147
+ }
148
+
149
+ function onAgentPost(
150
+ ctx: AgentPostContext,
151
+ config: TuringPulseOpenClawConfig,
152
+ sessionTracker: SessionTracker,
153
+ ): void {
154
+ const sessionKey = ctx.sessionKey;
155
+ if (!sessionKey) return;
156
+
157
+ const session = sessionTracker.get(sessionKey);
158
+ if (!session) return;
159
+
160
+ if (ctx.agentId && !session.agentId) {
161
+ session.agentId = ctx.agentId;
162
+ }
163
+
164
+ const promptTokens = ctx.usage?.promptTokens ?? 0;
165
+ const completionTokens = ctx.usage?.completionTokens ?? 0;
166
+
167
+ if (promptTokens > 0 || completionTokens > 0 || ctx.durationMs) {
168
+ const spanId = generateUUID();
169
+ const event = buildAgentReasoningSpan(ctx, session, spanId, config);
170
+ sessionTracker.addChildEvent(sessionKey, event);
171
+ }
172
+
173
+ sessionTracker.addMetrics(sessionKey, {
174
+ promptTokens,
175
+ completionTokens,
176
+ });
177
+ }
178
+
179
+ async function onMessageSent(
180
+ ctx: MessageSentContext,
181
+ config: TuringPulseOpenClawConfig,
182
+ buffer: EventBuffer,
183
+ sessionTracker: SessionTracker,
184
+ ): Promise<void> {
185
+ const sessionKey = ctx.sessionKey;
186
+ if (!sessionKey) return;
187
+
188
+ const session = sessionTracker.finish(sessionKey);
189
+ if (!session) return;
190
+
191
+ const inputText = '';
192
+ const outputText = ctx.context?.content ?? '';
193
+ const status = ctx.context?.success === false ? 'error' : 'success';
194
+ const error = ctx.context?.error;
195
+
196
+ const rootEvent = buildRootSpan(session, config, {
197
+ inputText,
198
+ outputText,
199
+ status,
200
+ error,
201
+ });
202
+
203
+ buffer.push(rootEvent);
204
+ buffer.pushMany(session.childEvents);
205
+
206
+ await buffer.flush();
207
+ }