autotel-genai 0.3.1 → 0.3.2
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/README.md +105 -4
- package/dist/agent/index.js +2 -1
- package/dist/agent-security-ChL0rIh-.js +181 -0
- package/dist/agent-security-ChL0rIh-.js.map +1 -0
- package/dist/{agent-DthMDQbW.js → agent-yHB-GF6g.js} +96 -3
- package/dist/agent-yHB-GF6g.js.map +1 -0
- package/dist/ai-sdk-bridge.cjs +31 -0
- package/dist/ai-sdk-bridge.cjs.map +1 -1
- package/dist/ai-sdk-bridge.d.cts +42 -1
- package/dist/ai-sdk-bridge.d.cts.map +1 -1
- package/dist/ai-sdk-bridge.d.ts +42 -1
- package/dist/ai-sdk-bridge.d.ts.map +1 -1
- package/dist/ai-sdk-bridge.js +30 -1
- package/dist/ai-sdk-bridge.js.map +1 -1
- package/dist/{index-CIkLg2LA.d.ts → index-BiTlgIvW.d.ts} +255 -2
- package/dist/index-BiTlgIvW.d.ts.map +1 -0
- package/dist/{index-CGHFYLGQ.d.cts → index-BkYnyG--.d.cts} +255 -2
- package/dist/index-BkYnyG--.d.cts.map +1 -0
- package/dist/index.cjs +7 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -4
- package/dist/observer/index.cjs +6 -2
- package/dist/observer/index.d.cts +2 -2
- package/dist/observer/index.d.ts +2 -2
- package/dist/observer/index.js +2 -2
- package/dist/observer-CNaW3fb1.js +1060 -0
- package/dist/observer-CNaW3fb1.js.map +1 -0
- package/dist/{observer-BbhXgZBA.cjs → observer-YBI0hQqC.cjs} +551 -14
- package/dist/observer-YBI0hQqC.cjs.map +1 -0
- package/package.json +3 -3
- package/skills/autotel-genai/SKILL.md +25 -3
- package/dist/agent-DthMDQbW.js.map +0 -1
- package/dist/context-B2wjX3O1.js +0 -73
- package/dist/context-B2wjX3O1.js.map +0 -1
- package/dist/index-CGHFYLGQ.d.cts.map +0 -1
- package/dist/index-CIkLg2LA.d.ts.map +0 -1
- package/dist/observer-2SKgcQI_.js +0 -613
- package/dist/observer-2SKgcQI_.js.map +0 -1
- package/dist/observer-BbhXgZBA.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -68,7 +68,8 @@ export const chat = traceGenAI({
|
|
|
68
68
|
| **Trace** | `autotel-genai/trace` | `traceGenAI()`, `recordGenAiResponse/Usage` |
|
|
69
69
|
| **Guard** | `autotel-genai/guard` | inline cost/token/loop kill-switch — `createGenAiBudget`, `createGenAiGuard`, `parseGuardRules` |
|
|
70
70
|
| **Streaming** | `autotel-genai/streaming` | TTFC, throughput, inter-chunk distribution — `createStreamTimer`, `recordStreamTiming` |
|
|
71
|
-
| **AI SDK** | `autotel-genai/
|
|
71
|
+
| **AI SDK** | `autotel-genai/observer` | `autotelTelemetry()` — `registerTelemetry()` integration: live `gen_ai.*` spans + cost + streaming + nested traces + opt-in content. `subscribeAiTelemetry()` — zero-config `ai:telemetry` channel path |
|
|
72
|
+
| **AI SDK (legacy)** | `autotel-genai/ai-sdk` | `ai.*` → `gen_ai.*` mapping + cost for `LegacyOpenTelemetry`/older versions; `autotelEnrich()` for `@ai-sdk/otel` `enrichSpan` |
|
|
72
73
|
| **Agents** | `autotel-genai/agent` | identity, delegation, policy, audit, privacy, non-repudiation |
|
|
73
74
|
|
|
74
75
|
### Cost
|
|
@@ -201,9 +202,75 @@ It honours spec breaking change #242 (`gen_ai.agent.id` is dropped on internal
|
|
|
201
202
|
|
|
202
203
|
### Vercel AI SDK
|
|
203
204
|
|
|
204
|
-
|
|
205
|
-
`
|
|
206
|
-
|
|
205
|
+
Register `autotelTelemetry()` once and every `generateText` / `streamText` /
|
|
206
|
+
`embed` call streams a canonical `gen_ai.*` span tree — live, as it runs:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { registerTelemetry } from 'ai';
|
|
210
|
+
import { autotelTelemetry } from 'autotel-genai/observer';
|
|
211
|
+
|
|
212
|
+
registerTelemetry(autotelTelemetry()); // once, at startup
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
See `apps/example-ai-sdk-observer` for a runnable AI SDK + Ollama demo
|
|
216
|
+
(generateText, tool loop, streamText timing, embeddings).
|
|
217
|
+
|
|
218
|
+
It implements the AI SDK's stable `Telemetry` lifecycle interface (ai v7+), so
|
|
219
|
+
it slots in exactly where `@ai-sdk/otel`'s `OpenTelemetry` does — but it also,
|
|
220
|
+
on every `chat` span:
|
|
221
|
+
|
|
222
|
+
- **prices the call** (`gen_ai.usage.cost.usd`) from `MODEL_PRICING`;
|
|
223
|
+
- records **streaming throughput** (`time_to_first_chunk`, `time_to_finish`,
|
|
224
|
+
`output_tokens_per_second`);
|
|
225
|
+
- keeps token usage on leaf `chat` spans only, so the `invoke_agent` root never
|
|
226
|
+
double-counts.
|
|
227
|
+
|
|
228
|
+
It is push-based and concurrency-safe (every event carries the SDK `callId`),
|
|
229
|
+
and it pulls in **no** dependency on `ai` — the returned object satisfies the
|
|
230
|
+
`Telemetry` interface structurally, so the snippet above type-checks as-is.
|
|
231
|
+
`rerank` has no canonical `gen_ai` operation and is intentionally not mapped.
|
|
232
|
+
|
|
233
|
+
**Nested traces.** It implements the SDK's `executeTool` / `executeLanguageModelCall`
|
|
234
|
+
context runners, so a tool whose `execute` calls `generateText` — and the
|
|
235
|
+
provider's own auto-instrumented HTTP spans — nest under the right span
|
|
236
|
+
automatically.
|
|
237
|
+
|
|
238
|
+
**Content capture (opt-in).** Off by default for privacy. Turn it on to record
|
|
239
|
+
prompts, responses, system instructions, and tool I/O, mapped to the
|
|
240
|
+
[GenAI SemConv message format](#genai-message-format). The SDK's per-call
|
|
241
|
+
`recordInputs` / `recordOutputs` are honored, and `exportContent` lets you redact
|
|
242
|
+
or drop content per event:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
registerTelemetry(
|
|
246
|
+
autotelTelemetry({
|
|
247
|
+
captureContent: true,
|
|
248
|
+
exportContent: (event) => redact(event), // optional: redact before write
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Zero-config (no `registerTelemetry`).** Subscribe to the SDK's `ai:telemetry`
|
|
254
|
+
Node tracing channel instead. The SDK publishes operation spans as soon as the
|
|
255
|
+
channel has a subscriber:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { subscribeAiTelemetry } from 'autotel-genai/observer';
|
|
259
|
+
|
|
260
|
+
const unsubscribe = subscribeAiTelemetry(); // once, at startup
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The channel path gives you the same `invoke_agent › chat › execute_tool` tree
|
|
264
|
+
with usage and cost, but not the per-call streaming timing (which only the
|
|
265
|
+
lifecycle `onLanguageModelCallEnd` event carries) — prefer
|
|
266
|
+
`registerTelemetry(autotelTelemetry())` when you can.
|
|
267
|
+
|
|
268
|
+
Register globally, or pass per-call via `telemetry.integrations` to scope it to
|
|
269
|
+
one call. For the **legacy** `LegacyOpenTelemetry`/older-version path, or to
|
|
270
|
+
enrich spans another integration already emitted, the attribute bridge maps
|
|
271
|
+
`ai.*` → `gen_ai.*` and adds cost; for versions before the `Telemetry` interface,
|
|
272
|
+
walk the finished result with `observeAiSdkResult` (see
|
|
273
|
+
[Observer](#observer-event-stream--spans)):
|
|
207
274
|
|
|
208
275
|
```ts
|
|
209
276
|
import { mapAiSdkAttributes, recordAiSdkCost } from 'autotel-genai/ai-sdk';
|
|
@@ -212,6 +279,40 @@ const canonical = mapAiSdkAttributes(span.attributes); // ai.* → gen_ai.*
|
|
|
212
279
|
recordAiSdkCost(ctx, span.attributes); // sets gen_ai.usage.cost.usd
|
|
213
280
|
```
|
|
214
281
|
|
|
282
|
+
#### Already using `@ai-sdk/otel`?
|
|
283
|
+
|
|
284
|
+
Drop `autotelEnrich()` into its `enrichSpan` to stamp autotel provenance and
|
|
285
|
+
promote your `runtimeContext` onto every span:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { OpenTelemetry } from '@ai-sdk/otel';
|
|
289
|
+
import { autotelEnrich } from 'autotel-genai/ai-sdk';
|
|
290
|
+
|
|
291
|
+
registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`enrichSpan` **cannot add cost** — the SDK passes it only
|
|
295
|
+
`{ spanType, operationId, callId, runtimeContext }` (no usage, no model), and its
|
|
296
|
+
own attributes win over custom keys. To get `gen_ai.usage.cost.usd` on the model
|
|
297
|
+
span, use `autotelTelemetry()` (it owns span creation). Either way,
|
|
298
|
+
[`autotel-devtools`](../autotel-devtools) prices `gen_ai` spans on render, so cost
|
|
299
|
+
shows there regardless of which integration emitted them.
|
|
300
|
+
|
|
301
|
+
#### Local devtools, one line
|
|
302
|
+
|
|
303
|
+
Point an OTLP exporter at a running `autotel-devtools` receiver and you get a
|
|
304
|
+
live GenAI run view — cost, token breakdown, tool timeline, and a narrated
|
|
305
|
+
"Explain run" walkthrough — that works in production too (unlike
|
|
306
|
+
`@ai-sdk/devtools`, which is dev-only):
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { registerTelemetry } from 'ai';
|
|
310
|
+
import { autotelTelemetry } from 'autotel-genai/observer';
|
|
311
|
+
|
|
312
|
+
registerTelemetry(autotelTelemetry()); // → your OTLP pipeline → autotel-devtools
|
|
313
|
+
// npx autotel-devtools → http://localhost:4318
|
|
314
|
+
```
|
|
315
|
+
|
|
215
316
|
### Observer (event-stream → spans)
|
|
216
317
|
|
|
217
318
|
When you instrument a framework that emits its own lifecycle stream (agent
|
package/dist/agent/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as recordControllerId, c as recordMemoryAccess, d as tryRecordHumanApproval, i as recordActiveScopes, l as recordPlanStep, n as deriveActionRiskClass, o as recordHumanApproval, p as agentContextFromSpan, r as recordActionRiskClass, s as recordInputProvenance, t as AGENT_SECURITY_ATTR, u as recordRenderOutput } from "../agent-security-ChL0rIh-.js";
|
|
2
|
+
import { C as canonicalizeForHash, S as createAgentAuditMetadata, T as AGENT_AUDIT_SCHEMA_VERSION, _ as recordPolicyDecision, a as withScopedTool, b as flattenAgentAttributes, c as resolvePrivacyProfile, d as withAgentSession, f as delegateToAgent, g as recordDecisionBasis, h as defineAgentToolCall, i as runAgentPlanClassifier, l as sanitizeAuditPayload, m as defineAgentAction, n as heuristicPlanRiskClassifier, o as createSignedEventEnvelope, p as recordAgentHandoff, r as recordPlanRiskAssessment, s as verifyEventEnvelopeHash, t as AGENT_PLAN_RISK_ATTR, u as createAgentIdentityRegistry, v as withAgentAction, w as hashPayload, x as setAgentAttributes, y as withAgentToolCall } from "../agent-yHB-GF6g.js";
|
|
2
3
|
|
|
3
4
|
export { AGENT_AUDIT_SCHEMA_VERSION, AGENT_PLAN_RISK_ATTR, AGENT_SECURITY_ATTR, agentContextFromSpan, canonicalizeForHash, createAgentAuditMetadata, createAgentIdentityRegistry, createSignedEventEnvelope, defineAgentAction, defineAgentToolCall, delegateToAgent, deriveActionRiskClass, flattenAgentAttributes, hashPayload, heuristicPlanRiskClassifier, recordActionRiskClass, recordActiveScopes, recordAgentHandoff, recordControllerId, recordDecisionBasis, recordHumanApproval, recordInputProvenance, recordMemoryAccess, recordPlanRiskAssessment, recordPlanStep, recordPolicyDecision, recordRenderOutput, resolvePrivacyProfile, runAgentPlanClassifier, sanitizeAuditPayload, setAgentAttributes, tryRecordHumanApproval, verifyEventEnvelopeHash, withAgentAction, withAgentSession, withAgentToolCall, withScopedTool };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { getTraceContext, otelTrace } from "autotel";
|
|
2
|
+
import { hashIdentifier } from "autotel-audit";
|
|
3
|
+
|
|
4
|
+
//#region src/agent/context.ts
|
|
5
|
+
const MISSING_CONTEXT_MESSAGE = "[autotel-genai] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve an agent context without throwing. Returns `null` when no trace context
|
|
8
|
+
* is available, so callers can degrade gracefully (best-effort instrumentation).
|
|
9
|
+
*/
|
|
10
|
+
const INVALID_TRACE_ID = "00000000000000000000000000000000";
|
|
11
|
+
function resolveContextSafe(ctx) {
|
|
12
|
+
if (ctx) return ctx;
|
|
13
|
+
const span = otelTrace.getActiveSpan();
|
|
14
|
+
if (!span) return null;
|
|
15
|
+
const ids = getTraceContext();
|
|
16
|
+
const sc = span.spanContext();
|
|
17
|
+
const traceId = ids?.traceId ?? sc.traceId;
|
|
18
|
+
if (!traceId || traceId === INVALID_TRACE_ID) return null;
|
|
19
|
+
return {
|
|
20
|
+
traceId,
|
|
21
|
+
spanId: ids?.spanId ?? sc.spanId,
|
|
22
|
+
correlationId: ids?.correlationId ?? traceId.slice(0, 16),
|
|
23
|
+
setAttribute: (key, value) => span.setAttribute(key, value),
|
|
24
|
+
setAttributes: (attrs) => span.setAttributes(attrs)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function resolveContext(ctx) {
|
|
28
|
+
const resolved = resolveContextSafe(ctx);
|
|
29
|
+
if (resolved) return resolved;
|
|
30
|
+
throw new Error(MISSING_CONTEXT_MESSAGE);
|
|
31
|
+
}
|
|
32
|
+
const warnedMissingContext = /* @__PURE__ */ new Set();
|
|
33
|
+
/** Warn (once per action) that an agent action is running without a trace context. */
|
|
34
|
+
function warnMissingContextOnce(action) {
|
|
35
|
+
if (warnedMissingContext.has(action)) return;
|
|
36
|
+
warnedMissingContext.add(action);
|
|
37
|
+
console.warn(`[autotel-genai] No active trace context for "${action}" — running un-audited. Wrap the call in trace()/instrument() or pass options.ctx to capture agent audit telemetry. (set options.onMissingContext: "throw" to fail fast, or "skip" to silence this warning)`);
|
|
38
|
+
}
|
|
39
|
+
/** Adapt an OpenTelemetry span (or span-like object) to {@link AgentContext}. */
|
|
40
|
+
function agentContextFromSpan(span) {
|
|
41
|
+
const sc = span.spanContext();
|
|
42
|
+
return {
|
|
43
|
+
traceId: sc.traceId,
|
|
44
|
+
spanId: sc.spanId,
|
|
45
|
+
correlationId: sc.traceId.slice(0, 16),
|
|
46
|
+
setAttribute: (key, value) => span.setAttribute(key, value),
|
|
47
|
+
setAttributes: (attrs) => span.setAttributes(attrs)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function toAttributeValue(value) {
|
|
51
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
if (value.every((entry) => typeof entry === "string")) return value;
|
|
54
|
+
if (value.every((entry) => typeof entry === "number")) return value;
|
|
55
|
+
if (value.every((entry) => typeof entry === "boolean")) return value;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
} catch {
|
|
59
|
+
return "<serialization-failed>";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (value instanceof Date) return value.toISOString();
|
|
63
|
+
if (typeof value === "bigint") return value.toString(10);
|
|
64
|
+
if (value === null || value === void 0) return;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(value);
|
|
67
|
+
} catch {
|
|
68
|
+
return "<serialization-failed>";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/agent/agent-security.ts
|
|
74
|
+
/** Canonical agent security attribute keys (Google SAIF / human-control aligned). */
|
|
75
|
+
const AGENT_SECURITY_ATTR = {
|
|
76
|
+
controllerId: "agent.controller.id",
|
|
77
|
+
inputProvenance: "agent.input.provenance",
|
|
78
|
+
actionRiskClass: "agent.action.risk_class",
|
|
79
|
+
consentRequired: "agent.consent.required",
|
|
80
|
+
consentOutcome: "agent.consent.outcome",
|
|
81
|
+
scopeActive: "agent.scope.active",
|
|
82
|
+
memoryOperation: "agent.memory.operation",
|
|
83
|
+
memoryIsolationKey: "agent.memory.isolation_key",
|
|
84
|
+
memoryContentHash: "agent.memory.content_hash",
|
|
85
|
+
planStepIndex: "agent.plan.step_index",
|
|
86
|
+
planToolIntents: "agent.plan.tool_intents",
|
|
87
|
+
planPolicyIds: "agent.plan.policy_ids",
|
|
88
|
+
outputFormat: "agent.output.format",
|
|
89
|
+
outputContainsUrl: "agent.output.contains_url",
|
|
90
|
+
outputUrlCount: "agent.output.url_count"
|
|
91
|
+
};
|
|
92
|
+
function setSecurityAttr(ctx, key, value) {
|
|
93
|
+
const attr = toAttributeValue(value);
|
|
94
|
+
if (attr === void 0) return;
|
|
95
|
+
const traceCtx = resolveContext(ctx);
|
|
96
|
+
if (Array.isArray(attr)) {
|
|
97
|
+
traceCtx.setAttributes({ [key]: attr });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
traceCtx.setAttribute(key, attr);
|
|
101
|
+
}
|
|
102
|
+
function setSecurityAttrs(ctx, attrs) {
|
|
103
|
+
const traceCtx = resolveContext(ctx);
|
|
104
|
+
const mapped = {};
|
|
105
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
106
|
+
const attr = toAttributeValue(value);
|
|
107
|
+
if (attr !== void 0) mapped[key] = attr;
|
|
108
|
+
}
|
|
109
|
+
if (Object.keys(mapped).length > 0) traceCtx.setAttributes(mapped);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Derive a coarse action risk class from MCP tool hints or explicit overrides.
|
|
113
|
+
*/
|
|
114
|
+
function deriveActionRiskClass(hints, override) {
|
|
115
|
+
if (override) return override;
|
|
116
|
+
if (hints.financial) return "financial";
|
|
117
|
+
if (hints.exfiltrationCapable || hints.openWorldHint) return "exfiltration_capable";
|
|
118
|
+
if (hints.destructiveHint) return "destructive";
|
|
119
|
+
if (hints.readOnlyHint) return "read";
|
|
120
|
+
if (hints.untrustedContentHint) return "read";
|
|
121
|
+
return "write";
|
|
122
|
+
}
|
|
123
|
+
function recordControllerId(input) {
|
|
124
|
+
const id = input.hash === false ? input.controllerId : hashIdentifier(input.controllerId, { salt: input.hashSalt });
|
|
125
|
+
setSecurityAttr(input.ctx, AGENT_SECURITY_ATTR.controllerId, id);
|
|
126
|
+
}
|
|
127
|
+
function recordInputProvenance(input) {
|
|
128
|
+
setSecurityAttr(input.ctx, AGENT_SECURITY_ATTR.inputProvenance, input.provenance);
|
|
129
|
+
}
|
|
130
|
+
function recordHumanApproval(input) {
|
|
131
|
+
const attrs = {
|
|
132
|
+
[AGENT_SECURITY_ATTR.consentRequired]: input.required ?? true,
|
|
133
|
+
[AGENT_SECURITY_ATTR.consentOutcome]: input.approved ? "approved" : "denied",
|
|
134
|
+
"tool.call.id": input.toolCallId
|
|
135
|
+
};
|
|
136
|
+
if (input.toolName) attrs["tool.name"] = input.toolName;
|
|
137
|
+
if (input.controllerId) attrs[AGENT_SECURITY_ATTR.controllerId] = hashIdentifier(input.controllerId, { salt: input.hashSalt });
|
|
138
|
+
setSecurityAttrs(input.ctx, attrs);
|
|
139
|
+
}
|
|
140
|
+
function recordActiveScopes(input) {
|
|
141
|
+
setSecurityAttr(input.ctx, AGENT_SECURITY_ATTR.scopeActive, input.scopes);
|
|
142
|
+
}
|
|
143
|
+
function recordActionRiskClass(riskClass, options = {}) {
|
|
144
|
+
setSecurityAttr(options.ctx, AGENT_SECURITY_ATTR.actionRiskClass, riskClass);
|
|
145
|
+
}
|
|
146
|
+
function recordMemoryAccess(input) {
|
|
147
|
+
setSecurityAttrs(input.ctx, {
|
|
148
|
+
[AGENT_SECURITY_ATTR.memoryOperation]: input.operation,
|
|
149
|
+
[AGENT_SECURITY_ATTR.memoryIsolationKey]: input.isolationKey,
|
|
150
|
+
...input.contentHash !== void 0 && { [AGENT_SECURITY_ATTR.memoryContentHash]: input.contentHash }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function recordPlanStep(input) {
|
|
154
|
+
setSecurityAttrs(input.ctx, {
|
|
155
|
+
[AGENT_SECURITY_ATTR.planStepIndex]: input.stepIndex,
|
|
156
|
+
...input.toolIntents !== void 0 && { [AGENT_SECURITY_ATTR.planToolIntents]: input.toolIntents },
|
|
157
|
+
...input.policyIds !== void 0 && { [AGENT_SECURITY_ATTR.planPolicyIds]: input.policyIds },
|
|
158
|
+
...input.summary !== void 0 && { "decision.summary": input.summary }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function recordRenderOutput(input) {
|
|
162
|
+
setSecurityAttrs(input.ctx, {
|
|
163
|
+
...input.format !== void 0 && { [AGENT_SECURITY_ATTR.outputFormat]: input.format },
|
|
164
|
+
...input.containsUrl !== void 0 && { [AGENT_SECURITY_ATTR.outputContainsUrl]: input.containsUrl },
|
|
165
|
+
...input.urlCount !== void 0 && { [AGENT_SECURITY_ATTR.outputUrlCount]: input.urlCount }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Best-effort variant — no throw when trace context is missing. */
|
|
169
|
+
function tryRecordHumanApproval(input) {
|
|
170
|
+
const ctx = resolveContextSafe(input.ctx);
|
|
171
|
+
if (!ctx) return false;
|
|
172
|
+
recordHumanApproval({
|
|
173
|
+
...input,
|
|
174
|
+
ctx
|
|
175
|
+
});
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
export { warnMissingContextOnce as _, recordControllerId as a, recordMemoryAccess as c, tryRecordHumanApproval as d, MISSING_CONTEXT_MESSAGE as f, toAttributeValue as g, resolveContextSafe as h, recordActiveScopes as i, recordPlanStep as l, resolveContext as m, deriveActionRiskClass as n, recordHumanApproval as o, agentContextFromSpan as p, recordActionRiskClass as r, recordInputProvenance as s, AGENT_SECURITY_ATTR as t, recordRenderOutput as u };
|
|
181
|
+
//# sourceMappingURL=agent-security-ChL0rIh-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-security-ChL0rIh-.js","names":[],"sources":["../src/agent/context.ts","../src/agent/agent-security.ts"],"sourcesContent":["import { getTraceContext, otelTrace } from 'autotel';\n\nexport interface AgentContext {\n traceId: string;\n spanId: string;\n correlationId: string;\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nexport const MISSING_CONTEXT_MESSAGE =\n '[autotel-genai] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an agent context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AgentContext): AgentContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself. This makes agent audit work in *any* OTel setup —\n // @effect/opentelemetry, a vanilla NodeSDK, autotel-cloudflare's instrumented\n // fetch handler — not just inside autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AgentContext): AgentContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nconst warnedMissingContext = new Set<string>();\n\n/** Warn (once per action) that an agent action is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-genai] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture agent audit telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** A no-op {@link AgentContext} whose attribute setters do nothing. */\nexport function noopAgentContext(): AgentContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\n/** Adapt an OpenTelemetry span (or span-like object) to {@link AgentContext}. */\nexport function agentContextFromSpan(span: {\n spanContext(): { traceId: string; spanId: string };\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}): AgentContext {\n const sc = span.spanContext();\n return {\n traceId: sc.traceId,\n spanId: sc.spanId,\n correlationId: sc.traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (typeof value === 'bigint') {\n return value.toString(10);\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { hashIdentifier } from 'autotel-audit';\nimport { resolveContext, resolveContextSafe, toAttributeValue, type AgentContext } from './context.js';\n\n/** Canonical agent security attribute keys (Google SAIF / human-control aligned). */\nexport const AGENT_SECURITY_ATTR = {\n controllerId: 'agent.controller.id',\n inputProvenance: 'agent.input.provenance',\n actionRiskClass: 'agent.action.risk_class',\n consentRequired: 'agent.consent.required',\n consentOutcome: 'agent.consent.outcome',\n scopeActive: 'agent.scope.active',\n memoryOperation: 'agent.memory.operation',\n memoryIsolationKey: 'agent.memory.isolation_key',\n memoryContentHash: 'agent.memory.content_hash',\n planStepIndex: 'agent.plan.step_index',\n planToolIntents: 'agent.plan.tool_intents',\n planPolicyIds: 'agent.plan.policy_ids',\n outputFormat: 'agent.output.format',\n outputContainsUrl: 'agent.output.contains_url',\n outputUrlCount: 'agent.output.url_count',\n} as const;\n\nexport type AgentInputProvenance =\n | 'user_direct'\n | 'user_voice'\n | 'rag'\n | 'memory'\n | 'tool_result'\n | 'external_untrusted';\n\nexport type AgentActionRiskClass =\n | 'read'\n | 'write'\n | 'destructive'\n | 'financial'\n | 'exfiltration_capable';\n\nexport type AgentConsentOutcome = 'approved' | 'denied' | 'timeout' | 'revoked';\n\nexport type AgentMemoryOperation = 'read' | 'write' | 'delete' | 'search';\n\nexport type AgentOutputFormat = 'text' | 'markdown' | 'html' | 'json' | 'mixed';\n\nexport interface AgentSecurityRecordOptions {\n ctx?: AgentContext;\n}\n\nexport interface RecordControllerInput extends AgentSecurityRecordOptions {\n /** Controlling human user id — hashed before emission unless already a digest. */\n controllerId: string;\n /** Pass a stable per-deployment salt to `hashIdentifier`. */\n hashSalt?: string;\n /** When false, emit the id as given (must not be raw PII). Default true. */\n hash?: boolean;\n}\n\nexport interface RecordInputProvenanceInput extends AgentSecurityRecordOptions {\n provenance: AgentInputProvenance;\n}\n\nexport interface RecordHumanApprovalInput extends AgentSecurityRecordOptions {\n toolCallId: string;\n toolName?: string;\n approved: boolean;\n required?: boolean;\n controllerId?: string;\n hashSalt?: string;\n}\n\nexport interface RecordActiveScopesInput extends AgentSecurityRecordOptions {\n scopes: string[];\n}\n\nexport interface RecordMemoryAccessInput extends AgentSecurityRecordOptions {\n operation: AgentMemoryOperation;\n isolationKey: string;\n contentHash?: string;\n}\n\nexport interface RecordPlanStepInput extends AgentSecurityRecordOptions {\n stepIndex: number;\n toolIntents?: string[];\n policyIds?: string[];\n summary?: string;\n}\n\nexport interface RecordRenderOutputInput extends AgentSecurityRecordOptions {\n format?: AgentOutputFormat;\n containsUrl?: boolean;\n urlCount?: number;\n}\n\nexport interface ActionRiskHints {\n readOnlyHint?: boolean;\n destructiveHint?: boolean;\n openWorldHint?: boolean;\n untrustedContentHint?: boolean;\n financial?: boolean;\n exfiltrationCapable?: boolean;\n}\n\nfunction setSecurityAttr(\n ctx: AgentContext | undefined,\n key: string,\n value: unknown,\n): void {\n const attr = toAttributeValue(value);\n if (attr === undefined) return;\n const traceCtx = resolveContext(ctx);\n if (Array.isArray(attr)) {\n traceCtx.setAttributes({ [key]: attr });\n return;\n }\n traceCtx.setAttribute(key, attr);\n}\n\nfunction setSecurityAttrs(\n ctx: AgentContext | undefined,\n attrs: Record<string, unknown>,\n): void {\n const traceCtx = resolveContext(ctx);\n const mapped: Record<string, string | number | boolean | string[] | number[] | boolean[]> =\n {};\n for (const [key, value] of Object.entries(attrs)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n mapped[key] = attr;\n }\n }\n if (Object.keys(mapped).length > 0) {\n traceCtx.setAttributes(mapped);\n }\n}\n\n/**\n * Derive a coarse action risk class from MCP tool hints or explicit overrides.\n */\nexport function deriveActionRiskClass(\n hints: ActionRiskHints,\n override?: AgentActionRiskClass,\n): AgentActionRiskClass {\n if (override) return override;\n if (hints.financial) return 'financial';\n if (hints.exfiltrationCapable || hints.openWorldHint) {\n return 'exfiltration_capable';\n }\n if (hints.destructiveHint) return 'destructive';\n if (hints.readOnlyHint) return 'read';\n if (hints.untrustedContentHint) return 'read';\n return 'write';\n}\n\nexport function recordControllerId(input: RecordControllerInput): void {\n const id =\n input.hash === false\n ? input.controllerId\n : hashIdentifier(input.controllerId, { salt: input.hashSalt });\n setSecurityAttr(input.ctx, AGENT_SECURITY_ATTR.controllerId, id);\n}\n\nexport function recordInputProvenance(input: RecordInputProvenanceInput): void {\n setSecurityAttr(input.ctx, AGENT_SECURITY_ATTR.inputProvenance, input.provenance);\n}\n\nexport function recordHumanApproval(input: RecordHumanApprovalInput): void {\n const attrs: Record<string, unknown> = {\n [AGENT_SECURITY_ATTR.consentRequired]: input.required ?? true,\n [AGENT_SECURITY_ATTR.consentOutcome]: input.approved ? 'approved' : 'denied',\n 'tool.call.id': input.toolCallId,\n };\n if (input.toolName) {\n attrs['tool.name'] = input.toolName;\n }\n if (input.controllerId) {\n attrs[AGENT_SECURITY_ATTR.controllerId] = hashIdentifier(input.controllerId, {\n salt: input.hashSalt,\n });\n }\n setSecurityAttrs(input.ctx, attrs);\n}\n\nexport function recordActiveScopes(input: RecordActiveScopesInput): void {\n setSecurityAttr(input.ctx, AGENT_SECURITY_ATTR.scopeActive, input.scopes);\n}\n\nexport function recordActionRiskClass(\n riskClass: AgentActionRiskClass,\n options: AgentSecurityRecordOptions = {},\n): void {\n setSecurityAttr(options.ctx, AGENT_SECURITY_ATTR.actionRiskClass, riskClass);\n}\n\nexport function recordMemoryAccess(input: RecordMemoryAccessInput): void {\n setSecurityAttrs(input.ctx, {\n [AGENT_SECURITY_ATTR.memoryOperation]: input.operation,\n [AGENT_SECURITY_ATTR.memoryIsolationKey]: input.isolationKey,\n ...(input.contentHash !== undefined && {\n [AGENT_SECURITY_ATTR.memoryContentHash]: input.contentHash,\n }),\n });\n}\n\nexport function recordPlanStep(input: RecordPlanStepInput): void {\n setSecurityAttrs(input.ctx, {\n [AGENT_SECURITY_ATTR.planStepIndex]: input.stepIndex,\n ...(input.toolIntents !== undefined && {\n [AGENT_SECURITY_ATTR.planToolIntents]: input.toolIntents,\n }),\n ...(input.policyIds !== undefined && {\n [AGENT_SECURITY_ATTR.planPolicyIds]: input.policyIds,\n }),\n ...(input.summary !== undefined && { 'decision.summary': input.summary }),\n });\n}\n\nexport function recordRenderOutput(input: RecordRenderOutputInput): void {\n setSecurityAttrs(input.ctx, {\n ...(input.format !== undefined && {\n [AGENT_SECURITY_ATTR.outputFormat]: input.format,\n }),\n ...(input.containsUrl !== undefined && {\n [AGENT_SECURITY_ATTR.outputContainsUrl]: input.containsUrl,\n }),\n ...(input.urlCount !== undefined && {\n [AGENT_SECURITY_ATTR.outputUrlCount]: input.urlCount,\n }),\n });\n}\n\n/** Best-effort variant — no throw when trace context is missing. */\nexport function tryRecordHumanApproval(input: RecordHumanApprovalInput): boolean {\n const ctx = resolveContextSafe(input.ctx);\n if (!ctx) return false;\n recordHumanApproval({ ...input, ctx });\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { estimateLLMCost } from "./cost.js";
|
|
2
2
|
import { i as genAiResponseAttributes, r as genAiRequestAttributes, s as genAiUsageAttributes } from "./attributes-DU-PmLqZ.js";
|
|
3
|
-
import {
|
|
3
|
+
import { _ as warnMissingContextOnce, f as MISSING_CONTEXT_MESSAGE, g as toAttributeValue, h as resolveContextSafe, m as resolveContext } from "./agent-security-ChL0rIh-.js";
|
|
4
4
|
import { createNoopRequestLogger, createStructuredError, getRequestLoggerSafe } from "autotel";
|
|
5
5
|
import { forceKeepAuditEvent, securityEvent, withAudit } from "autotel-audit";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -903,5 +903,98 @@ async function withScopedTool(definition, input, fn, options = {}) {
|
|
|
903
903
|
}
|
|
904
904
|
|
|
905
905
|
//#endregion
|
|
906
|
-
|
|
907
|
-
|
|
906
|
+
//#region src/agent/agent-plan-classifier.ts
|
|
907
|
+
/** Canonical plan-risk attribute keys (Google SAIF plan-risk predictor aligned). */
|
|
908
|
+
const AGENT_PLAN_RISK_ATTR = {
|
|
909
|
+
verdict: "agent.plan.risk.verdict",
|
|
910
|
+
score: "agent.plan.risk.score",
|
|
911
|
+
categories: "agent.plan.risk.categories",
|
|
912
|
+
toolSequence: "agent.plan.risk.tool_sequence"
|
|
913
|
+
};
|
|
914
|
+
function setPlanRiskAttrs(ctx, attrs) {
|
|
915
|
+
const traceCtx = resolveContext(ctx);
|
|
916
|
+
const mapped = {};
|
|
917
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
918
|
+
const attr = toAttributeValue(value);
|
|
919
|
+
if (attr !== void 0) mapped[key] = attr;
|
|
920
|
+
}
|
|
921
|
+
if (Object.keys(mapped).length > 0) traceCtx.setAttributes(mapped);
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Stamp plan-risk assessment attrs on the active span.
|
|
925
|
+
*/
|
|
926
|
+
function recordPlanRiskAssessment(options) {
|
|
927
|
+
const { assessment, toolSequence } = options;
|
|
928
|
+
setPlanRiskAttrs(options.ctx, {
|
|
929
|
+
[AGENT_PLAN_RISK_ATTR.verdict]: assessment.verdict,
|
|
930
|
+
...assessment.score !== void 0 && { [AGENT_PLAN_RISK_ATTR.score]: assessment.score },
|
|
931
|
+
...assessment.categories?.length && { [AGENT_PLAN_RISK_ATTR.categories]: assessment.categories },
|
|
932
|
+
...toolSequence?.length && { [AGENT_PLAN_RISK_ATTR.toolSequence]: toolSequence },
|
|
933
|
+
...assessment.reason !== void 0 && { "decision.summary": assessment.reason }
|
|
934
|
+
});
|
|
935
|
+
if (options.emitSecurityEvent && assessment.verdict !== "low") securityEvent({
|
|
936
|
+
name: "llm.plan.risk.elevated",
|
|
937
|
+
category: "llm",
|
|
938
|
+
outcome: assessment.verdict === "critical" ? "blocked" : "denied",
|
|
939
|
+
severity: assessment.verdict === "critical" ? "critical" : assessment.verdict === "high" ? "error" : "warning",
|
|
940
|
+
reason: assessment.reason ?? assessment.verdict,
|
|
941
|
+
...assessment.score !== void 0 && { score: assessment.score },
|
|
942
|
+
...assessment.categories?.length && { categories: assessment.categories.join(",") }
|
|
943
|
+
}, { ctx: options.ctx });
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Run a pluggable plan-risk classifier and record its verdict on the span.
|
|
947
|
+
* Classifier failures degrade quietly (no assessment recorded).
|
|
948
|
+
*/
|
|
949
|
+
async function runAgentPlanClassifier(classifier, input, options = {}) {
|
|
950
|
+
let assessment;
|
|
951
|
+
try {
|
|
952
|
+
assessment = await classifier(input);
|
|
953
|
+
} catch {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (!assessment) return void 0;
|
|
957
|
+
recordPlanRiskAssessment({
|
|
958
|
+
...options,
|
|
959
|
+
assessment,
|
|
960
|
+
toolSequence: input.toolSequence
|
|
961
|
+
});
|
|
962
|
+
return assessment;
|
|
963
|
+
}
|
|
964
|
+
const DESTRUCTIVE_TOOL = /\b(delete|remove|send|post|transfer|pay|upload|execute)\b/i;
|
|
965
|
+
const UNTRUSTED_READ = /\b(read|fetch|get|search|load|parse|inbox|email|web|scrape)\b/i;
|
|
966
|
+
/**
|
|
967
|
+
* Dependency-free first-pass plan-risk heuristic. Opt-in — pass as
|
|
968
|
+
* `AgentPlanClassifier` or wrap your own Model Armor / Llama Guard adapter.
|
|
969
|
+
*/
|
|
970
|
+
function heuristicPlanRiskClassifier() {
|
|
971
|
+
return ({ toolSequence }) => {
|
|
972
|
+
if (toolSequence.length === 0) return {
|
|
973
|
+
verdict: "low",
|
|
974
|
+
score: 0
|
|
975
|
+
};
|
|
976
|
+
const normalized = toolSequence.map((name) => name.replaceAll("_", " "));
|
|
977
|
+
const hasDestructive = normalized.some((name) => DESTRUCTIVE_TOOL.test(name));
|
|
978
|
+
const hasUntrustedRead = normalized.some((name) => UNTRUSTED_READ.test(name));
|
|
979
|
+
if (hasDestructive && hasUntrustedRead) return {
|
|
980
|
+
verdict: "high",
|
|
981
|
+
score: .85,
|
|
982
|
+
categories: ["untrusted_to_destructive_chain"],
|
|
983
|
+
reason: "mixed_untrusted_and_destructive_tools"
|
|
984
|
+
};
|
|
985
|
+
if (toolSequence.length >= 8) return {
|
|
986
|
+
verdict: "medium",
|
|
987
|
+
score: .55,
|
|
988
|
+
categories: ["long_tool_chain"],
|
|
989
|
+
reason: "long_tool_sequence"
|
|
990
|
+
};
|
|
991
|
+
return {
|
|
992
|
+
verdict: "low",
|
|
993
|
+
score: .1
|
|
994
|
+
};
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
//#endregion
|
|
999
|
+
export { canonicalizeForHash as C, createAgentAuditMetadata as S, AGENT_AUDIT_SCHEMA_VERSION as T, recordPolicyDecision as _, withScopedTool as a, flattenAgentAttributes as b, resolvePrivacyProfile as c, withAgentSession as d, delegateToAgent as f, recordDecisionBasis as g, defineAgentToolCall as h, runAgentPlanClassifier as i, sanitizeAuditPayload as l, defineAgentAction as m, heuristicPlanRiskClassifier as n, createSignedEventEnvelope as o, recordAgentHandoff as p, recordPlanRiskAssessment as r, verifyEventEnvelopeHash as s, AGENT_PLAN_RISK_ATTR as t, createAgentIdentityRegistry as u, withAgentAction as v, hashPayload as w, setAgentAttributes as x, withAgentToolCall as y };
|
|
1000
|
+
//# sourceMappingURL=agent-yHB-GF6g.js.map
|