AGT + Provedit · Integration reference
AGT → Provedit over OTLP
Microsoft Agent Governance Toolkit v3.5.0 shipped OTelLogsBackend, a built-in audit backend that emits every AuditEntry as an OpenTelemetry LogRecord. Provedit accepts those records on an OTLP/HTTP endpoint, re-decides policy on a different trust boundary, and appends both verdicts to a hash-linked chain. This is observability and compliance evidence, not a network gate. AGT decides and runs in-process; Provedit records and re-judges after the fact.
Prerequisites
- AGT (
agent-governance-toolkit) v3.5.0 or newer.OTelLogsBackendwas added in PR #1747; older versions don't have it. - A Provedit agent key (
pvk_…) from app.provedit.ai/account/agent-keys. Scope it to one tenant. - One of: an OpenTelemetry Collector you control (recommended), or AGT's direct OTLP exporter (no collector).
Endpoint
POST https://api.provedit.ai/otlp/v1/logs
Authorization: Bearer pvk_…
Content-Type: application/json
Standard OTLP/HTTP path. An OTel Collector configured with endpoint: https://api.provedit.ai/otlp appends /v1/logs itself. Direct exporters must use the full URL above.
encoding: json on your exporter. Protobuf returns 415 Unsupported Media Type with a pointer at this knob; we'll add protobuf when a customer needs it.
Recipe A: AGT → OTel Collector → Provedit (recommended)
Run a Collector (sidecar or host agent). It handles batching, retries, queueing, and protocol negotiation, so a Provedit outage never costs you audit records.
1. Attach the OTel backend to AGT
from agent_os.audit_logger import GovernanceAuditLogger
from agent_os.audit_logger.otel import OTelLogsBackend
audit = GovernanceAuditLogger()
audit.add_backend(OTelLogsBackend()) # reads OTEL_EXPORTER_OTLP_* env vars
2. Configure the Collector
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
timeout: 5s
send_batch_size: 100
exporters:
otlphttp/provedit:
endpoint: https://api.provedit.ai/otlp
encoding: json
headers:
Authorization: "Bearer ${PROVEDIT_API_KEY}"
sending_queue:
enabled: true
queue_size: 5000
retry_on_failure:
enabled: true
max_elapsed_time: 5m
service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/provedit]
3. Point AGT at the Collector
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://localhost:4318/v1/logs
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
Recipe B: AGT direct OTLP exporter
No Collector. AGT's OTelLogsBackend posts to Provedit directly. Simpler to stand up; you lose Collector-side queueing and any failure mid-batch is on you to retry.
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://api.provedit.ai/otlp/v1/logs
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_LOGS_HEADERS="Authorization=Bearer ${PROVEDIT_API_KEY}"
Verify with curl
Hand-craft an ExportLogsServiceRequest and post it. Useful for first-time setup, CI smoke tests, and proving credentials work before debugging an exporter.
curl -sS -X POST https://api.provedit.ai/otlp/v1/logs \
-H "Authorization: Bearer $PROVEDIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"resourceLogs": [{
"scopeLogs": [{
"logRecords": [{
"timeUnixNano": "'"$(date +%s)000000000"'",
"body": { "stringValue": "{\"hash\":\"'"$(openssl rand -hex 32)"'\",\"agent_id\":\"smoke-bot\",\"action\":\"smoke.curl\",\"decision\":\"allow\",\"context\":{\"note\":\"hello\"}}" },
"attributes": [
{ "key": "event.type", "value": { "stringValue": "policy_decision" } },
{ "key": "agent.id", "value": { "stringValue": "smoke-bot" } },
{ "key": "governance.action", "value": { "stringValue": "smoke.curl" } },
{ "key": "governance.decision", "value": { "stringValue": "allow" } }
]
}]
}]
}]
}'
A clean accept returns {}. Partial failures return { "partialSuccess": { "rejectedLogRecords": N, "errorMessage": "…" } }. The chain entry should appear in the operator console timeline within a second.
Field mapping
Provedit reads the agt.* attribute namespace and the JSON-encoded AuditEntry body. Attributes outside this namespace are ignored by the receiver (no crash, but not stored). Anything under agt.audit.meta.* is preserved verbatim under params._agt.meta.
| Source (OTLP) | Destination (Provedit action) |
|---|---|
attribute governance.action | tool (required; missing → record rejected as partialSuccess) |
attribute agent.id | actor.agentName, actor.vendor = "agt" |
attribute event.type | actionKind: policy_decision → invocation, tool_call|tool_result → result, other → event |
attribute governance.decision | params._agt.decision (hint; Provedit re-decides) |
attribute governance.latency_ms | params._agt.latencyMs |
attribute agt.audit.meta.session_id | sessionId |
attribute agt.audit.meta.* | params._agt.meta.* (prefix stripped, verbatim) |
body .context (or .metadata) | params (top level; hashed into the chain) |
body .hash | params._agt.entryHash (also intra-batch dedupe key) |
body .previous_hash | params._agt.previousEntryHash |
observedTimeUnixNano (else timeUnixNano) | requestedAt |
Choose a policy for the agent key
Every pvk_ key is bound to one named policy. Because the OTLP path is post-hoc, that binding only decides what kind of signal you want from Provedit's re-decision, not whether traffic flows.
-
Pure observability, no second opinion. Bind the key to the built-in
agt.observepolicy. It returnsallowfor every tool. Every AGT record commits to the chain withpolicyDecision: allow;params._agt.decisionstill preserves AGT's verdict alongside. Use this when you only want the tamper-evident system of record and don't care about disagreement detection. -
Disagreement detection. Bind the key to your real production policy (or to the built-in
provedit.mcp.default). Provedit re-evaluates every record and surfaces any case where its verdict differs from AGT's. The post-hocrequire_approvalresult is a finding to alert on and tighten policy from, not an approval ticket: the OTLP receiver never opens approval workflows because the action already ran.
You can pick when minting the key in app.provedit.ai/account/agent-keys, or switch later by minting a new key bound to a different policy and rotating it in your Collector / env.
Why the policy lives off the host
AGT's policy is a file that ships next to the agent. That works for honest operators, but it is the same threat model as an EDR whose exclusion list lives on the endpoint: whoever has write access to the box can quietly loosen a rule, then run the action, then put the rule back. The on-host audit log records what the on-host policy said at the time, which is exactly what the attacker just edited.
Provedit holds the policy in the control plane. The agent key (pvk_) is bound to a policy name; the active version of that policy is fetched, version-pinned, and re-evaluated server-side for every record that arrives over OTLP. The host never sees the policy text and cannot edit it. The chain entry commits to the policy version id, so an auditor can prove which rules were in force when each action ran, not which rules the host claimed were in force.
Two verdicts, one of them post-hoc
AGT decides and executes in-process, then emits the LogRecord. By the time Provedit sees it, the tool has already run. We are not in the call path; we cannot block, queue, or require approval for something that already happened.
What we do is re-decide on a different trust boundary and stamp the verdict on the chain. AGT's verdict lives at params._agt.decision. Provedit's lives at policyDecision. Both are hashed into the same entry, both signed by the chain. The pair is the artefact: compliance evidence that two independent policy engines saw the same record and agreed (or didn't).
Mismatches are the high-value rows. Both verdicts ride on the same chain entry, so a query against GET /v1/actions (or the chain export) that compares params._agt.decision with policyDecision surfaces every case where AGT's local policy let a call through that your central Provedit policy would have rejected, or vice versa. These are the records to alert on, retro-review, and tighten policy from. There is no approval workflow on a mismatch; the action already ran.
If you want Provedit in the call path, blocking before execution, use the SDK or the MCP proxy instead. See /docs. The OTLP receiver is for the case where you've already standardised on AGT and want a tamper-evident system of record with a second policy opinion attached.
Failure modes
| Response | Meaning | Collector behaviour |
|---|---|---|
200 {} | All records accepted and on-chain. | Discard batch. |
200 partialSuccess | Some records rejected (e.g. missing governance.action). errorMessage names the reasons. | Discard rejected records; does not retry. Fix your emitter. |
401 | Bearer token missing or wrong. | Retries until backoff cap, then dead-letters. |
409 no_active_policy | The agent key is bound to a policy that has no active version on this tenant. | Retries. Fix in app.provedit.ai/policies. |
415 | Content-Type was protobuf. Set encoding: json on the exporter. | Will keep failing until config changes. |
429 + Retry-After | Global rate limit hit (600 req/min/IP). | Honors Retry-After; standard OTel backoff. |
503 | Backend temporarily unavailable. | Retries with backoff. |
Provedit does not gate AGT's tool calls; we are a record, not an interceptor. A network outage between AGT and Provedit means delayed entries, never blocked operations. The OTel Collector's sending_queue + retry_on_failure are what keep records from being lost during the outage.
Rate limits & quotas
OTLP traffic shares the global rate limit of 600 requests per minute per source IP. One Collector pushing batches almost never bumps it; a misconfigured loop will. 429 responses include both the draft-7 RateLimit-* headers and a classic Retry-After in seconds, which the OTel Collector honors by default.
One OTLP batch counts as one HTTP request regardless of how many LogRecords it contains. One LogRecord counts as one action against your plan's monthly action include. Plan limits and overage policy live on the pricing page.
Verify the chain
OTLP entries are indistinguishable from direct POST /v1/actions entries on the chain. The same verifier replays them:
npx @provedit/verifier verify --from 1 --to 1000
See Verify the chain in the SDK docs for the full check list, anchor format, and CI workflow.