@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,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
|
+
}
|