{"_id":"@altheris/sdk","_rev":"2-5669065527a56a0ad7d7cea1ae9a438c","name":"@altheris/sdk","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@altheris/sdk","version":"0.1.0","keywords":["ai","agents","security","llm","prompt-injection","guardrails","openai","anthropic","langchain"],"license":"MIT","_id":"@altheris/sdk@0.1.0","maintainers":[{"name":"olliepurbrick","email":"olliepurbrick@gmail.com"}],"homepage":"https://altheris.io","bugs":{"url":"https://github.com/olliepurbrick/Altheris/issues"},"dist":{"shasum":"03246dd490d6a46e5c21c32401dbae30baeb60df","tarball":"https://registry.npmjs.org/@altheris/sdk/-/sdk-0.1.0.tgz","fileCount":9,"integrity":"sha512-lCycqszXnlPgJnKlFekCmV/sA/BlcFwuNNLqCV3LZnGhgdBjVnS1WZEa4pDlFeGe/9EMkUpwnuVfIPR36RsA7A==","signatures":[{"sig":"MEQCIAeu9RpiGv30p/VskPmJ+NLnbxOI7mW0HdqaqR+xxrtIAiBtCa5B5yZHfaAqq5ueU+tvb/43faZUd7B1EvBcxEx02g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1681194},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.17"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"46fcd6cae2f8c1afcfb2b7bcf5fdb290284c5464","scripts":{"test":"vitest run","build":"tsup","prepack":"npm run build","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"olliepurbrick","email":"olliepurbrick@gmail.com"},"repository":{"url":"git+https://github.com/olliepurbrick/Altheris.git","type":"git","directory":"packages/sdk"},"_npmVersion":"11.11.1","description":"Runtime security for AI agents — scope enforcement, prompt-injection screening, output redaction, human approvals, cost circuit breakers, honeypots.","directories":{},"sideEffects":false,"_nodeVersion":"25.8.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","openai":"^7.4.0","vitest":"^4.1.8","typescript":"^5","@types/node":"^20","@anthropic-ai/sdk":"^0.116.0"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.1.0_1787792251791_0.9070694271594542","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@altheris/sdk@0.2.0","bugs":{"url":"https://github.com/olliepurbrick/Altheris/issues"},"dist":{"shasum":"ba200cd23657a086c8002afcae500905275e4386","tarball":"https://registry.npmjs.org/@altheris/sdk/-/sdk-0.2.0.tgz","fileCount":9,"integrity":"sha512-/PfXFgXtWTHl4D2rCKxiRsv/bTa3+os29sHChxHUROUJtuvE4cr4NjDuZ6BcdP1FmhrrxWm2h9SkmIomFKb9jQ==","signatures":[{"sig":"MEYCIQCq0tqhMKNHJTjdrBeeUsLLM997JVRVrWfHWzZn9SVVLwIhAJptvIc5SoIivgpYj6BF1RFygbe9Gx6tO+Yk1jodbXVT","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDJjyZPipBUu4RztvlT59H/o0mePz+Iw9QmUlwZJtdJaAiEAgHQPR5yrsBx/nlMxjwNYf6mtmDdi8Q617Sq9it0TQbs="}],"unpackedSize":1698044},"main":"./dist/index.cjs","name":"@altheris/sdk","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.17"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"08049956c52f6e5d6833deb557c7c383fa7577a5","license":"MIT","scripts":{"test":"vitest run","build":"tsup","prepack":"npm run build","typecheck":"tsc --noEmit","test:watch":"vitest"},"version":"0.2.0","_npmUser":{"name":"olliepurbrick","email":"olliepurbrick@gmail.com"},"homepage":"https://altheris.io","keywords":["ai","agents","security","llm","prompt-injection","guardrails","openai","anthropic","langchain"],"repository":{"url":"git+https://github.com/olliepurbrick/Altheris.git","type":"git","directory":"packages/sdk"},"_npmVersion":"11.11.1","description":"Runtime security for AI agents — scope enforcement, prompt-injection screening, output redaction, human approvals, cost circuit breakers, honeypots.","directories":{},"maintainers":[{"name":"olliepurbrick","email":"olliepurbrick@gmail.com"}],"sideEffects":false,"_nodeVersion":"25.8.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","openai":"^7.4.0","vitest":"^4.1.8","typescript":"^5","@types/node":"^20","@anthropic-ai/sdk":"^0.116.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sdk_0.2.0_1789238076081_0.8395964929522968"}}},"time":{"created":"2026-08-27T00:57:31.606Z","modified":"2026-09-12T18:34:36.392Z","0.1.0":"2026-08-27T00:57:31.947Z","0.2.0":"2026-09-12T18:34:36.210Z"},"bugs":{"url":"https://github.com/olliepurbrick/Altheris/issues"},"license":"MIT","homepage":"https://altheris.io","keywords":["ai","agents","security","llm","prompt-injection","guardrails","openai","anthropic","langchain"],"repository":{"url":"git+https://github.com/olliepurbrick/Altheris.git","type":"git","directory":"packages/sdk"},"description":"Runtime security for AI agents — scope enforcement, prompt-injection screening, output redaction, human approvals, cost circuit breakers, honeypots.","maintainers":[{"name":"olliepurbrick","email":"olliepurbrick@gmail.com"}],"readme":"# @altheris/sdk\n\nRuntime security for AI agents. Wrap your LLM client in one line and every\ncall is screened for prompt injection, held to the agent's declared scope,\nfiltered for leaked secrets/PII, gated behind human approval where you've\nrequired it, cost-tracked against a circuit breaker, and subject to an\nemergency kill switch — enforced by the Altheris platform, not by prompts.\n\n## Quick start\n\n```bash\nnpm install @altheris/sdk\n```\n\nPut the agent's `alsk_` SDK key in your `.env` — never commit this file:\n\n```bash\nALTHERIS_API_KEY=alsk_your_sdk_key\n```\n\nThen wrap the LLM client you already construct:\n\n```ts\nimport { protect } from \"@altheris/sdk\";\n\nconst client = protect(new OpenAI(), { agentId: \"your-agent-uuid\" });\n```\n\n`protect()` also takes `onBlocked` (every block event) and `onError` (telemetry\nand fail-open availability errors). With no `onBlocked`, each tool call the\nscope gate strips is logged once with `console.warn`, naming the tool and the\nreason — pass `onBlocked` to receive the event instead, or `onBlocked: () => {}`\nto silence it.\n\nprotect() auto-detects OpenAI-like, Anthropic-like, LangChain runnable, and Vercel AI model clients, and throws on anything it does not recognise — it never leaves a client silently unprotected.\n\nEvery `chat.completions.create` call now runs the full security pipeline.\nGet your `alsk_` SDK key from **Dashboard → Agent → SDK key**.\n\n## Advanced: explicit configuration\n\n`protect()` is the one-call path and reads `ALTHERIS_API_KEY` from the\nenvironment. Construct the class directly when you need to configure anything\nelse — a key provider function, `baseUrl`, the other callbacks, or `close()`:\n\n```ts\nimport { Altheris } from \"@altheris/sdk\";\n\nconst altheris = new Altheris({ apiKey: \"alsk_…\", agentId: \"your-agent-uuid\" });\nconst openai = altheris.protectOpenAI(new OpenAI());\n```\n\n## What you get on every call\n\n| # | Feature | How |\n|---|---------|-----|\n| 1 | Emergency lockdown (kill switch) | polled every 30s; locked agents refuse all calls |\n| 2 | Conditional access policies | time windows enforced locally; IP/geo server-side |\n| 3 | Prompt-injection screening | last user message → platform pattern corpus |\n| 4 | Runtime scope enforcement | every call checked against allow/deny action globs |\n| 5 | Human-in-the-loop approval | flagged actions wait for an approver (or time out) |\n| 6 | Output filtering / redaction | credit cards, SSNs, API keys, JWTs, passwords… |\n| 7 | Cost circuit breaker | usage reported per call; runaway spend auto-pauses |\n| 8 | Decision provenance | forensic record of every protected call |\n| 9 | Honeypot canaries | bait URLs detected → agent suspended instantly |\n| 10–13 | Config versioning, behavioural baselining, threat signals, audit trail | server-side, fed by the SDK |\n\n## Adapters\n\n### OpenAI\n\n```ts\nimport OpenAI from \"openai\";\nimport { Altheris } from \"@altheris/sdk\";\n\nconst altheris = new Altheris({ apiKey: process.env.ALTHERIS_API_KEY!, agentId: AGENT_ID });\nconst openai = altheris.protectOpenAI(new OpenAI());\n\nconst res = await openai.chat.completions.create({\n  model: \"gpt-4o\",\n  messages: [{ role: \"user\", content: userMessage }],\n});\n// res.choices[0].message.content is already redacted if it leaked anything\n```\n\nWraps `chat.completions.create` and legacy `completions.create`. Streaming\n(`stream: true`) is fully supported — see [Streaming](#streaming).\n\nThe legacy `completions.create` has no `tools` parameter and returns no tool\ncalls, so the tool gates are **N/A** there by API shape — not a coverage gap.\n\n### Anthropic\n\n```ts\nimport Anthropic from \"@anthropic-ai/sdk\";\n\nconst anthropic = altheris.protectAnthropic(new Anthropic());\n\nconst msg = await anthropic.messages.create({\n  model: \"claude-sonnet-4-6\",\n  max_tokens: 1024,\n  messages: [{ role: \"user\", content: userMessage }],\n});\n```\n\nWraps `messages.create`. String and content-block-array messages both\nscreened; response text blocks filtered individually.\n\n### LangChain (JS 0.3.x)\n\n```ts\nconst chain = prompt.pipe(model).pipe(parser);\nconst safeChain = altheris.protectLangChain(chain);\n\nconst answer = await safeChain.invoke({ input: userMessage });\n```\n\nWraps `invoke`, `batch` and `stream` of any Runnable. Inputs may be\nstrings, `{ input | question | text | content | query }` objects, or message\narrays. Cost is best-effort from `usage_metadata` /\n`response_metadata.tokenUsage` when the chain surfaces them.\n\n**Response tool calls are screened (default on).** The `tool_calls` on a\nreturned message — and the `tool_call_chunks` / `tool_calls` streamed by\n`stream` — are checked against the pinned tool registry and then against the\nagent's intent grants before your executor loop can iterate them. A call that\nfails either check is removed (streamed calls are buffered and only emitted\nonce they pass), and each removal fires `onBlocked`. Disable with\n`toolRegistry: false` / `toolCallScopeGate: false`.\n\nBecause this fails closed, note the LangChain-specific consequence: LangChain\nbinds tools *inside* the Runnable, so if Altheris cannot sniff them and you did\nnot pass `tools`, nothing was pinned — and registry enforcement will then strip\n**every** tool call in the response. That is on top of the loud `onError` the\nadapter already fires when sniffing fails. Pass the tools explicitly:\n\n```ts\nconst safeChain = altheris.protectLangChain(chain, { tools: [myTool, otherTool] });\n```\n\n### Vercel AI SDK\n\n```ts\nimport { openai } from \"@ai-sdk/openai\";\nimport { generateText } from \"ai\";\n\nconst model = altheris.protectVercelAI(openai(\"gpt-4o\"));\nconst { text } = await generateText({ model, prompt: userMessage });\n```\n\nWraps the **model object** (`doGenerate`/`doStream`, v1 and v2 specs), so\n`generateText`, `streamText`, `generateObject` and `streamObject` are all\ncovered with one wrap.\n\n**Response tool calls are screened (default on).** The `toolCalls[]` / `content[]`\ntool-call parts on a `doGenerate` result *and* the streamed tool-call parts on\n`doStream` are checked against the pinned tool registry and then against the agent's\nintent grants **before the AI SDK's tool-execution loop ever sees them** — so a call\nthat fails either check never runs. Streamed calls are buffered while they arrive and\nemitted only once they pass; a denied call reaches your stream in no form at all, while\ntext parts keep flowing live. Disable with `toolRegistry: false` /\n`toolCallScopeGate: false`.\n\nRemovals are reported server-side by BOTH layers, and both fire `onBlocked`, but\nthey count differently — worth knowing before you count events. A scope-gate\nremoval fires `onBlocked` once per removed **call**, so two calls to the same\nungranted tool give you two events; a registry removal fires once per distinct\nblocked **name**, so duplicate calls to the same blocked tool collapse into a\nsingle event. Server-side, a registry removal is reported to `report-tool-block`\nas it always was, and a scope-gate strip sends **one** `report-tool-block` request\nper response (`checkpoint: \"tool_call_scope\"`, carrying each stripped tool with\nits intent and the reason it was refused) — fire-and-forget, never awaited, never\non your response path. And a tool call whose name cannot be read at all is\nstripped fail-closed by the registry path **without** an event, because there is\nno name to report.\n\nBecause this fails closed, note the consequence for the AI SDK: with the gates\non by default, a tool the server has not mapped to an intent — or an agent with\nno intent grants at all — has *every* one of its tool calls stripped, on both\n`doGenerate` and `doStream`. Absence is denial, not an oversight: a tool is\ncallable only once its intent is ticked for the agent in the dashboard.\n\n### Anything else — generic `protect()`\n\n```ts\nconst safeAgent = altheris.protect(myAgent, {\n  inputMethods: [\"sendMessage\"],        // 1st string arg → input screening + scope gate\n  outputMethods: [\"sendMessage\", \"receiveResponse\"], // primary string of result → filtering\n});\n```\n\nProxy-based: the original object (and its TypeScript type) is untouched;\nnamed methods get the pipeline. The result's \"primary string\" is the value\nitself if it's a string, else the first of `text` / `content` / `output` /\n`response` / `message` / `answer` / `result`.\n\n**`toolMethods` are gated at invocation (default on).** This is the one adapter\nwhere a tool's actual EXECUTION can be stopped — the method is invoked *through*\nAltheris, so a blocked call simply never runs, rather than merely being stripped\nfrom a response. Two checks fire, registry first: the tool's **pinned registry\nstatus** (a revoked or suspended tool throws before it executes), then the Layer 2\n**intent scope** gate. Each block throws `AltherisBlockedError` with\n`context.executed === false` and fires `onBlocked`. Disable with\n`toolRegistry: false` / `toolCallScopeGate: false`.\n\n**The scope gate here has a deliberate limit — read this before relying on it.**\nUnlike the other adapters, it enforces **only** when *both* of these hold: an\nintent snapshot has been received from Altheris (i.e. this agent has made at least\none `check-action`, which means listing the method in `inputMethods` too), **and**\nyou declared tool definitions via the `tools:` config. Anything less passes\nthrough **unenforced**. That is intentional: a generic `toolMethod` is only a\nmethod *name*, and a `toolMethods`-only agent never calls `check-action`, so\napplying absence-is-denial unconditionally would block every tool method of every\ngeneric agent that never touched the tool-definition workflow. Registry status\nenforcement is unaffected and applies regardless. To get scope enforcement on a\ngeneric agent, declare `tools:` and list the method in `inputMethods` as well:\n\n```ts\nconst altheris = new Altheris({ /* … */ tools: [processRefund, sendEmail] });\nconst safeAgent = altheris.protect(myAgent, {\n  inputMethods: [\"processRefund\", \"sendEmail\"],  // supplies the intent snapshot\n  toolMethods:  [\"processRefund\", \"sendEmail\"],  // …which the invocation gate reads\n});\n```\n\n## Configuration\n\n```ts\nconst altheris = new Altheris({\n  apiKey: \"alsk_…\",              // required — the agent's SDK key\n  agentId: \"…\",                  // required — the agent's UUID\n  endUserId: () => currentUser(),// string | () => string — explicit end-user identity,\n                                 // for per-end-user escalation. Never inferred; ≤256\n                                 // chars; omit for anonymous (escalation is skipped,\n                                 // never widened to the whole agent).\n  baseUrl: \"https://altheris.io\",// default\n  failureMode: \"fail-safe\",      // \"fail-safe\" (default) | \"fail-open\"\n  timeout: 5000,                 // per-request ms\n  cacheConfigTTL: 60000,         // agent-config cache\n  lockdownPollInterval: 30000,   // kill-switch poll\n  approvalPollInterval: 5000,    // poll while waiting on a human\n  approvalTimeout: undefined,    // default: the approval's own expiry\n  onBlocked: (e) => {},          // { phase, reason, agentId, detail }; default warns once per scope-stripped tool call\n  onRedacted: (e) => {},         // { agentId, redactions, postStream? }\n  onError: (err) => {},          // telemetry + fail-open availability errors\n  debug: false,\n});\n```\n\n**fail-safe vs fail-open.** `failureMode` governs *availability* failures\nonly. fail-safe (default): if Altheris is unreachable, the call is blocked\nwith `AltherisNetworkError`. fail-open: the call proceeds and `onError`\nfires. Server verdicts (blocked input, out-of-scope action) and the human\napproval gate **always** bind in both modes — an unreachable approval flow\nnever defaults to \"approved\".\n\n## Error handling\n\n```ts\nimport {\n  AltherisError,            // base — code, retryable, context\n  AltherisBlockedError,     // .phase: \"input\"|\"action\"|\"policy\"|\"honeypot\", .reason\n  AltherisLockdownError,    // kill switch engaged\n  AltherisApprovalRejectedError, // .approvalId, .reason\n  AltherisApprovalExpiredError,  // retryable: true — re-request the action\n  AltherisAuthError,        // bad alsk_ key / wrong agent — fix config\n  AltherisNetworkError,     // Altheris unreachable — retryable\n} from \"@altheris/sdk\";\n\ntry {\n  await openai.chat.completions.create(/* … */);\n} catch (e) {\n  if (e instanceof AltherisBlockedError) {\n    return `Request refused (${e.phase}): ${e.reason}`;\n  }\n  if (e instanceof AltherisNetworkError && e.retryable) {\n    /* back off and retry */\n  }\n  throw e;\n}\n```\n\n## Streaming\n\nStreams are passed through **live and unaltered** — redaction cannot be\napplied retroactively to chunks the caller has already received. The SDK\naccumulates the streamed text and runs output screening at stream end; if\nanything sensitive surfaced, `onRedacted` fires with `postStream: true` and\nthe detection is logged server-side. Input screening and the scope/approval\ngates still run **before** the stream opens. If post-hoc detection isn't\nacceptable for your use case, use non-streaming calls.\n\n**Cleanup is guaranteed even if you leave early.** If the consumer stops a\nstream before it ends — `break`, `return`, a thrown error, or cancelling the\nstream — the SDK still screens what was delivered and reports cost +\nprovenance. In that case the screened text is only what the consumer actually\nreceived (not the full LLM output), and `onRedacted` carries `partial: true`\nso you can tell the difference:\n\n```ts\nconst altheris = new Altheris({\n  apiKey, agentId,\n  onRedacted: (e) => {\n    if (e.partial) log.warn(\"sensitive content in a partially-read stream\", e.redactions);\n  },\n});\n\nfor await (const chunk of stream) {\n  if (enough(chunk)) break; // cleanup still runs on the delivered text\n}\n```\n\n## Honeypots\n\nCanary URLs from your agent's config are detected automatically in any text\nthe pipeline screens — taking the bait suspends the agent immediately. Two\nmanual hooks for layers the adapters can't see:\n\n```ts\nawait altheris.scanForHoneypots(urlAboutToBeFetched);     // throws on a canary\nawait altheris.reportHoneypotTrigger(honeypotId, { path }); // from your HTTP layer\n```\n\n## Latency\n\nBudget: **<150ms SDK overhead per call**; in practice the warm path is two\nparallel POSTs before the LLM call (input + action screening, both edge-\ndeployed) and one after (output). Agent config is cached 60s, lockdown\nstate 30s (background poll), cost/provenance reporting never blocks. Call\n`altheris.close()` on shutdown to stop the poller (it's unref'd, so it\nwon't hold your process open either way).\n\n## Notes\n\n- Node ≥ 18.17. Zero runtime dependencies. ESM and CommonJS.\n- The SDK never mutates your framework's response objects — redacted\n  responses are clones (class instances like LangChain's `AIMessage` are\n  rewritten in place to preserve their prototype).\n- Full docs: https://altheris.io/docs (coming soon).\n","readmeFilename":"README.md"}