# TypeScript SDK

> Full reference for the Thoth TypeScript SDK — ThothConfig, instrument(), wrapAnthropicTools(), wrapOpenAITools(), ThothClient, EnforcementMode, ThothPolicyViolation.

Source: https://docs.atensecurity.com/docs/sdks/typescript

**Reference examples:**
  Start with [Your first governed agent](https://docs.atensecurity.com/docs/getting-started/quickstart) for
  a complete runnable example and tested package version. Examples here focus
  on individual APIs and may depend on callbacks or clients defined by your
  application. Replace illustrative tenant and endpoint values with the
  supplied connection details; agree on server policy before using real tools.

## Installation

```bash
npm install @atensec/thoth
# or
pnpm add @atensec/thoth
# or
yarn add @atensec/thoth
```

**Node.js 22+ required.** The package ships full TypeScript declarations and requires a runtime
with `fetch` + `AbortSignal.timeout` support.

---

Preferred module APIs:

- `instrument()`
- `wrapAnthropicTools()`
- `wrapOpenAITools()`

Legacy compatibility:

- `ThothClient` remains supported for backward compatibility.
- `ThothClient.wrap()` is a legacy alias to `ThothClient.instrument()`.

```typescript
import { ThothClient } from "@atensec/thoth";

const client = new ThothClient({
  tenantId: "acme-corp",
  userId: "alice@acme.com",
});

client.instrument(agent, {
  agentId: "invoice-processor-v2",
  approvedScope: ["search_docs"],
  apiUrl: "https://enforce.acme-corp.atensecurity.com",
});
```

---

## ThothConfig

```typescript
interface ThothConfig {
  agentId: string;
  approvedScope: string[];
  tenantId: string;
  userId?: string; // default: "system"
  enforcement?: EnforcementMode; // default: EnforcementMode.BLOCK
  apiKey?: string; // or set THOTH_API_KEY env var
  apiUrl?: string; // required, or set THOTH_API_URL env var
  failOpen?: boolean; // default: false; retryable failures can allow when true
  stepUpTimeoutMinutes?: number; // default: 15
  stepUpPollIntervalMs?: number; // default: 5000
  sessionIntent?: string; // optional HIPAA minimum-necessary session scope
  policyContext?: Record<string, unknown>; // optional metadata.policy_context payload
  enforcementTraceId?: string; // default: generated session UUID
  environment?: string; // default: "prod"
}
```

### Fields

| Field                  | Type              | Default                     | Description                                |
| ---------------------- | ----------------- | --------------------------- | ------------------------------------------ |
| `agentId`              | `string`          | required                    | Unique identifier for this agent           |
| `approvedScope`        | `string[]`        | required                    | Intended tool names; server policy still applies |
| `tenantId`             | `string`          | required                    | Your Thoth tenant identifier               |
| `userId`               | `string`          | `"system"`                  | User initiating the request                |
| `enforcement`          | `EnforcementMode` | `BLOCK`                     | How to handle policy violations            |
| `apiKey`               | `string`          | `process.env.THOTH_API_KEY` | API key for authentication                 |
| `apiUrl`               | `string`          | `process.env.THOTH_API_URL` | Required tenant API base URL used for both `/v1/enforce` and `/v1/events/batch` |
| `failOpen` | `boolean` | `false` | Permit execution on retryable enforcer failure when enabled |
| `stepUpTimeoutMinutes` | `number`          | `15`                        | Max wait time for step-up approval         |
| `stepUpPollIntervalMs` | `number`          | `5000`                      | Step-up polling interval in milliseconds   |
| `sessionIntent`        | `string`          | unset                       | Session purpose used for HIPAA minimum-necessary checks |
| `policyContext`        | `Record<string, unknown>` | unset                | Tenant policy context sent as `metadata.policy_context` |
| `enforcementTraceId`   | `string`          | generated session UUID      | Correlation ID propagated to enforcer/services |
| `environment`          | `string`          | `"prod"`                    | Environment tag for env-scoped policy resolution |

---

## EnforcementMode

```typescript
enum EnforcementMode {
  OBSERVE = "observe", // log only, never block
  STEP_UP = "step_up", // require human approval
  BLOCK = "block", // immediately reject violations
  PROGRESSIVE = "progressive", // escalating enforcement
}
```

---

## ThothClient

`ThothClient` is a compatibility facade that stores default config values and delegates to
module-level APIs.

```typescript
class ThothClient {
  constructor(defaults?: Partial<ThothConfig>);

  instrument<T extends object>(agent: T, config?: Partial<ThothConfig>): T;
  instrumentAnthropic(
    toolFns: Record<string, AnthropicToolFn>,
    config?: Partial<ThothConfig>,
  ): Record<string, AnthropicToolFn>;
  instrumentOpenAI(
    toolFns: Record<string, OpenAIToolFn>,
    config?: Partial<ThothConfig>,
  ): Record<string, OpenAIToolFn>;

  // Legacy aliases:
  wrap<T extends object>(agent: T, config?: Partial<ThothConfig>): T;
  wrapAnthropicTools(
    toolFns: Record<string, AnthropicToolFn>,
    config?: Partial<ThothConfig>,
  ): Record<string, AnthropicToolFn>;
  wrapOpenAITools(
    toolFns: Record<string, OpenAIToolFn>,
    config?: Partial<ThothConfig>,
  ): Record<string, OpenAIToolFn>;
}
```

---

## instrument()

Instrument a generic AI agent with Thoth governance. Wraps all tools with enforce/emit hooks.
Returns the same agent object (mutated in-place).

```typescript
function instrument<T extends object>(agent: T, config: ThothConfig): T;
```

### Agent shape

The agent object must have a `tools` array where each element has a `name: string` and `run: Function`:

```typescript
const agent = {
  tools: [
    {
      name: "search_docs",
      run: async (query: string) => `results for: ${query}`,
    },
    {
      name: "submit_payment",
      run: async (invoiceId: string) => `payment submitted: ${invoiceId}`,
    },
  ],
};
```

### Example

```typescript
import {
  instrument,
  EnforcementMode,
  ThothPolicyViolation,
} from "@atensec/thoth";

const agent = {
  tools: [
    { name: "search_docs", run: async (q: string) => `docs: ${q}` },
    { name: "submit_payment", run: async (id: string) => `paid: ${id}` },
  ],
};

instrument(agent, {
  agentId: "invoice-processor-v2",
  approvedScope: ["search_docs"], // submit_payment is out of scope
  tenantId: "acme-corp",
  apiUrl: "https://enforce.acme-corp.atensecurity.com",
  enforcement: EnforcementMode.BLOCK,
});

// Intended tool: server policy must permit this call
const result = await agent.tools[0].run("quarterly report");

// Refused or pending-approval call: handled according to the server decision
try {
  await agent.tools[1].run("INV-001");
} catch (err) {
  if (err instanceof ThothPolicyViolation) {
    console.error(`Blocked: ${err.reason} (violation_id=${err.violationId})`);
  }
}
```

### Async generators

`instrument()` detects async generator functions automatically and wraps them correctly, yielding
all chunks while still emitting pre/post behavioral events:

```typescript
const streamingTool = {
  name: "stream_response",
  async *run(query: string) {
    for (const chunk of chunks) {
      yield chunk;
    }
  },
};
```

### Failure behavior

With the default `failOpen: false`, policy checks fail closed: if `/v1/enforce` is unreachable or returns a
non-OK response, the decision falls back to `BLOCK` (`reason: "enforcer unavailable"`), and
`instrument()` will throw `ThothPolicyViolation` before running the tool.

---

## emitBehavioralEvent()

Emit a single behavioral event directly to the Thoth API. Fire-and-forget — never blocks execution.

```typescript
import { emitBehavioralEvent } from "@atensec/thoth";
// also available as a named import from the package

async function emitBehavioralEvent(
  event: BehavioralEvent,
  apiUrl: string,
  apiKey: string,
): Promise<void>;
```

The function catches all errors internally and never throws. If the API is unreachable, the error
is silently discarded to preserve agent availability.

### BehavioralEvent schema

```typescript
interface BehavioralEvent {
  eventId: string; // UUID — generated automatically by instrument()
  tenantId: string;
  agentId?: string;
  sessionId: string;
  toolName?: string;
  userId: string;
  sourceType: SourceType; // "agent_tool_call" for SDK-wrapped tools
  eventType: EventType; // "TOOL_CALL_PRE" | "TOOL_CALL_POST" | "LLM_INVOCATION"
  content: string; // JSON-serialized arguments or result
  metadata?: Record<string, unknown>;
  approvedScope: string[];
  enforcementMode: EnforcementMode;
  sessionToolCalls: string[]; // tool names called so far in this session
  occurredAt: Date;
}
```

---

## wrapAnthropicTools()

Wrap tool execution functions for use in an Anthropic Claude agentic loop. Returns a new map of
governed callables.

```typescript
import { wrapAnthropicTools } from "@atensec/thoth/anthropic";

function wrapAnthropicTools(
  toolFns: Record<string, AnthropicToolFn>,
  config: ThothConfig,
): Record<string, AnthropicToolFn>;

type AnthropicToolFn = (
  input: Record<string, unknown>,
) => unknown | Promise<unknown>;
```

### Example

```typescript
import Anthropic from "@anthropic-ai/sdk";
import { wrapAnthropicTools } from "@atensec/thoth/anthropic";

const client = new Anthropic();

const toolFns = {
  search_docs: async (input: Record<string, unknown>) => {
    const query = input.query as string;
    return `docs for: ${query}`;
  },
};

const governed = wrapAnthropicTools(toolFns, {
  agentId: "support-bot",
  approvedScope: ["search_docs"],
  tenantId: "acme-corp",
  apiUrl: "https://enforce.acme-corp.atensecurity.com",
});

// Standard Anthropic agentic loop
const tools: Anthropic.Tool[] = [
  {
    name: "search_docs",
    description: "Search company documentation",
    input_schema: {
      type: "object" as const,
      properties: { query: { type: "string" } },
      required: ["query"],
    },
  },
];

const messages: Anthropic.MessageParam[] = [
  { role: "user", content: "Find docs about expense reports" },
];

while (true) {
  const response = await client.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 1024,
    tools,
    messages,
  });

  if (response.stop_reason === "end_turn") break;

  const toolResults: Anthropic.ToolResultBlockParam[] = [];
  for (const block of response.content) {
    if (block.type === "tool_use") {
      const fn = governed[block.name];
      if (fn) {
        const result = await fn(block.input as Record<string, unknown>);
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: String(result),
        });
      }
    }
  }

  messages.push({ role: "assistant", content: response.content });
  messages.push({ role: "user", content: toolResults });
}
```

---

## wrapOpenAITools()

Wrap tool execution functions for use in an OpenAI tool-calling loop.

```typescript
import { wrapOpenAITools } from "@atensec/thoth/openai";

function wrapOpenAITools(
  toolFns: Record<string, OpenAIToolFn>,
  config: ThothConfig,
): Record<string, OpenAIToolFn>;

type OpenAIToolFn = (
  args: Record<string, unknown>,
) => unknown | Promise<unknown>;
```

### Example

```typescript
import OpenAI from "openai";
import { wrapOpenAITools } from "@atensec/thoth/openai";

const openai = new OpenAI();

const governed = wrapOpenAITools(
  {
    search_docs: async (args) => `docs for: ${args.query}`,
  },
  {
    agentId: "support-bot",
    approvedScope: ["search_docs"],
    tenantId: "acme-corp",
    apiUrl: "https://enforce.acme-corp.atensecurity.com",
  },
);

const response = await openai.chat.completions.create({
  model: "gpt-4o",
  tools: [
    {
      type: "function",
      function: {
        name: "search_docs",
        description: "Search company docs",
        parameters: {
          type: "object",
          properties: { query: { type: "string" } },
        },
      },
    },
  ],
  messages: [{ role: "user", content: "Find docs about expense reports" }],
});

for (const toolCall of response.choices[0].message.tool_calls ?? []) {
  const fn = governed[toolCall.function.name];
  if (fn) {
    const args = JSON.parse(toolCall.function.arguments) as Record<
      string,
      unknown
    >;
    const result = await fn(args); // governance runs here
    console.log(result);
  }
}
```

---

## ThothPolicyViolation

Thrown when the enforcer blocks a tool call.

```typescript
class ThothPolicyViolation extends Error {
  public readonly decisionReasonCode?: string;
  public readonly actionClassification?: string;
  public readonly authorizationDecision?: string;
  public readonly deferTimeoutSeconds?: number;
  public readonly stepUpTimeoutSeconds?: number;
  public readonly riskScore?: number;
  public readonly latencyMs?: number;
  public readonly packId?: string;
  public readonly packVersion?: string;
  public readonly ruleVersion?: number;
  public readonly regulatoryRegimes?: string[];
  public readonly matchedRuleIds?: string[];
  public readonly matchedControlIds?: string[];
  public readonly policyReferences?: string[];
  public readonly modelSignals?: string[];
  public readonly receipt?: Record<string, unknown>;

  constructor(
    public readonly toolName: string,
    public readonly reason: string,
    public readonly violationId?: string,
  )
}
```

```typescript
import { ThothPolicyViolation } from "@atensec/thoth";

try {
  await governedTool(input);
} catch (err) {
  if (err instanceof ThothPolicyViolation) {
    console.error({
      tool: err.toolName,
      reason: err.reason,
      violationId: err.violationId, // correlate with Thoth evidence records
      decisionReasonCode: err.decisionReasonCode,
      actionClassification: err.actionClassification,
      riskScore: err.riskScore,
      packId: err.packId,
      matchedRuleIds: err.matchedRuleIds,
      modelSignals: err.modelSignals,
    });
    return { error: "Action blocked by policy" };
  }
  throw err;
}
```

---

## EnforcementDecision

The enforcer response shape (returned from `POST /v1/enforce`):

```typescript
interface EnforcementDecision {
  decision: DecisionType; // "ALLOW" | "BLOCK" | "STEP_UP" | "MODIFY" | "DEFER"
  authorizationDecision?: string;
  decisionReasonCode?: string;
  actionClassification?: string;
  reason?: string;
  violationId?: string;
  holdToken?: string; // present when decision === "STEP_UP"
  riskScore?: number;
  latencyMs?: number;
  packId?: string;
  packVersion?: string;
  ruleVersion?: number;
  regulatoryRegimes?: string[];
  matchedRuleIds?: string[];
  matchedControlIds?: string[];
  policyReferences?: string[];
  modelSignals?: string[];
  receipt?: Record<string, unknown>;
  modifiedToolArgs?: Record<string, unknown>; // present when decision === "MODIFY"
  modificationReason?: string; // present when decision === "MODIFY"
  deferReason?: string; // present when decision === "DEFER"
  deferTimeoutSeconds?: number; // present when decision === "DEFER"
  stepUpTimeoutSeconds?: number;
}

enum DecisionType {
  ALLOW = "ALLOW",
  BLOCK = "BLOCK",
  STEP_UP = "STEP_UP",
  MODIFY = "MODIFY",
  DEFER = "DEFER",
}
```

---

## Environment Variables

| Variable              | Description |
| --------------------- | ----------- |
| `THOTH_API_KEY`       | API key for hosted Thoth authentication |
| `THOTH_API_URL`       | Required tenant API base URL for both policy checks and event ingestion |
| `THOTH_LOG_LEVEL`     | Optional SDK decision-log level override (`DEBUG`, `INFO`, `WARN`, `ERROR`); falls back to `LOG_LEVEL` |

The TypeScript SDK reads these environment variables automatically in Node.js environments.
If `config.apiUrl` and `THOTH_API_URL` are both missing, `instrument()` throws at startup.
When decision logging is enabled at debug level, SDK logs include `hold_token` for `STEP_UP` flows.

---

## TypeScript strict mode

The SDK is compiled with `strict: true`. All exported types are narrowed — no implicit `any`.
If you use `instrument<T>()`, the generic `T` preserves your agent's type through the instrumentation:

```typescript
interface MyAgent {
  tools: Array<{ name: string; run: (input: string) => Promise<string> }>
  someOtherField: string
}

const agent: MyAgent = { tools: [...], someOtherField: "hello" }
const governed = instrument(agent, config)
// governed is still typed as MyAgent
console.log(governed.someOtherField)  // ✓ TypeScript happy
```
