@avanor/sdk
v0.2.0
Published
Avanor SDK — wrap your agents, evidence flows back
Maintainers
Readme
@avanor/sdk
Avanor SDK — wrap your agents, evidence flows back.
A first-party customer-installed SDK that captures, gates, and attests every
agent action against the Avanor governance platform. Drop it in alongside your
existing agent stack: telemetry, policy decisions, and cryptographic
attestations land in /dashboard/audit-log without restructuring your code.
Install
npm install @avanor/sdkQuickstart
// avanor.ts (one file, top of customer's app)
import { Avanor } from '@avanor/sdk';
export const avanor = Avanor.init({
apiKey: process.env.AVANOR_API_KEY!, // tenant binding derived server-side
environment: process.env.NODE_ENV ?? 'dev', // 'dev' | 'staging' | 'prod'
service: 'broker-portal', // becomes service.name on every span
});// in any handler:
import { avanor } from './avanor';
await avanor.track('user_login', { user_id });
const { allow, reason } = await avanor.allow('email_send', { to, subject });
if (!allow) throw new Error(`Blocked: ${reason}`);That is the entire surface a customer touches to land a row in
/dashboard/audit-log. Target: ≤ 5 minutes from npm install to first
visible event.
Multi-tenant (one process, many tenants)
Avanor.init() is a process singleton, the right default for a single-tenant
app. When one process must emit to multiple Avanor tenants (a background worker
servicing several customers, or an agency hosting multiple end-customers), use
createClient() instead. Each call returns a fully isolated client with its
own API key, queue, and lifecycle. The init() singleton is never touched.
import { createClient } from '@avanor/sdk';
const tenantA = createClient({
apiKey: process.env.AVANOR_KEY_A!,
environment: 'prod',
service: 'tenant-a-worker',
});
const tenantB = createClient({
apiKey: process.env.AVANOR_KEY_B!,
environment: 'prod',
service: 'tenant-b-worker',
});
tenantA.track('job_processed', { jobId });
tenantB.track('job_processed', { jobId });
// track() is fire-and-forget; flush() returns a Promise, so the worker can
// drain every tenant's queue before it exits.
await Promise.all([tenantA.flush(), tenantB.flush()]);Three modes
| Mode | Method | Purpose |
|---|---|---|
| Track | avanor.track(event, attrs?) | Passive telemetry. Fire-and-forget. |
| Allow | avanor.allow(action, attrs?) → Decision | Pre-action gate. Server-authoritative. |
| Attest | avanor.attest(action, payload) → Attestation | Post-action audit-log entry with hash chain. |
Configuration
The minimum is apiKey, environment, service. Everything else has a
sensible default. Full reference: https://docs.avanor.ai/sdk/.
Failure modes
fail-soft(default) —allow()returns{ allow: true, reason }on unreachable + warn-logs. Caller keeps moving.fail-open— same return, no warn.fail-closed— throwsAvanorUnreachable(network/timeout) orAvanorMalformedResponse(server replied junk). Caller must catch.
Kill switch
Three layered paths (spec §H.5):
- Server-issued long-poll →
enabled=falseflips the SDK to no-op state. AVANOR_SDK_DISABLE=1env var → forces no-op at process start.- Per-instrumentation disable via
disabled_instrumentations[]from the control endpoint.
A no-op transition always emits an sdk_killed lifecycle event first — the
kill cannot be silent.
Auto-discovery (zero-config)
Install the SDK and run your existing app with the auto-discovery loader — no code edits:
npm install @avanor/sdk
node --import @avanor/sdk/auto your-app.js
# or, for your whole environment:
export NODE_OPTIONS='--import @avanor/sdk/auto'The SDK detects OpenAI, Anthropic, the Vercel AI SDK, and raw fetch/undici
calls to provider hosts, and builds an inventory of every place AI runs in your
app. It coexists with Avanor.init() (the wrap-your-call-sites model above) and
with your existing OpenTelemetry setup.
On Next.js the loader still works; if a bundler defeats module patching, the
backwards-compatible instrumentModules: { openAI } escape hatch remains
available. Dev and prod report different feature identities on purpose: in
development (or under Turbopack) the SDK drops chunk-hashed call-site paths so a
coding session does not spawn dozens of phantom features.
Coexistence with your OpenTelemetry pipeline
node --import @avanor/sdk/auto evaluates Avanor's loader before your
application graph. On that path Avanor registers its own OpenTelemetry
TracerProvider and an AsyncLocalStorage context manager first, so Avanor
becomes the global OpenTelemetry owner for the discovery rail. This is by design:
the discovery rail needs an active provider in place before your app imports
openai / @anthropic-ai/sdk so the vendor-SDK patches land.
What this means for you:
You do not run your own OpenTelemetry. Nothing to do. Avanor owns the global provider and the discovery rail just works.
You DO run your own OpenTelemetry pipeline. Do not call a second
setGlobalTracerProvider(...)orNodeTracerProvider.register()after Avanor's--import. The OpenTelemetry API only accepts ONE global provider; the second call is a no-op against the API and logs a warning, so your spans would not reach your own exporters. Instead, add Avanor alongside your pipeline as a fan-out (see the docs link below). If, in the uncommon case, your provider is registered before Avanor's loader runs, Avanor detects the existing provider and attaches as a pure span-consumer automatically (it registers no competing provider, no context manager, and no vendor instrumentations in that mode).You want Avanor out of the global path entirely. Set
AVANOR_SDK_DISABLE=1. The rail becomes a complete no-op at process start: no provider, no context manager, no instrumentation, no events. This is the hard escape hatch.You want to see what Avanor would discover without sending anything. Set
AVANOR_DRY_RUN=true. Avanor emits only the hourly self-report (feature counts, no span detail) and ships no inventory data.
Privacy
- Content is NOT captured by default.
recordInputs/recordOutputsdefault tofalse. Prompt and response content is captured ONLY when you set BOTHAVANOR_CAPTURE_CONTENT=trueANDOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true(defense-in-depth — either flag alone keeps capture OFF). - Redaction cannot be disabled. When content capture is on, every captured string passes through a redaction pipeline first (emails, SSNs, credit cards, JWTs, AWS keys, OpenAI/Anthropic API keys, plus your own patterns). There is no flag to turn redaction off.
- Content lifetime is shorter than event lifetime. Captured content is retained for 7 days maximum, regardless of plan tier — shorter than the per-tier event/rollup retention. This is a hard privacy commitment.
- No hidden phone-home. The SDK ships exactly one telemetry channel about
itself: the hourly
avanor.self.reportaudit event. It contains only{ tenant_id, timestamp, discovery_count_last_hour, top_5_features_by_call_count, sdk_version, content_capture_enabled, redaction_pipeline_version }— no PII, no request bodies, no response bodies. It is printed to your STDERR so you can see exactly what it says, and you can disable it withAVANOR_SELF_REPORT=false. - Dry run. Set
AVANOR_DRY_RUN=trueto emit ONLY the self-report (feature counts, no span detail) — run it for as long as you like before any inventory data reaches Avanor.
Docs
https://docs.avanor.ai/sdk/ (full reference + recipe pages land in Slot F).
License
Avanor Source License v1 (BSL-modeled, 4-year Apache 2.0 sunset) — see LICENSE.
