npm package: @haltstate/sdk
Description: HaltState SDK - Runtime governance for AI agents
Author: HaltState
Latest version: 1.0.2

# @haltstate/sdk

Official TypeScript/JavaScript SDK for HaltState - Human-in-the-loop governance for AI agents.

## Installation

```bash
npm install @haltstate/sdk
# or
yarn add @haltstate/sdk
# or
pnpm add @haltstate/sdk
```

## Quick Start

```typescript
import { HaltStateClient, ApprovalPending, ActionDenied } from '@haltstate/sdk';

const client = new HaltStateClient({
  tenantId: 'your-tenant-id',
  apiKey: 'hs_your_api_key',
  agentId: 'payment-processor-01',
});

// Guard a high-risk action
try {
  const permit = await client.guard('process_payment', {
    params: { amount: 5000, currency: 'USD', recipient: 'vendor@example.com' },
    idempotencyKey: `payment-${orderId}`,
  });

  // Action approved - execute it
  await processPayment(orderId);

  // Report success with the permit
  await client.report(permit, { success: true, transactionId: txId });
} catch (error) {
  if (error instanceof ApprovalPending) {
    console.log(`Waiting for approval: ${error.approvalId}`);
    // Exit gracefully - retry later via scheduler
    process.exit(0);
  }
  if (error instanceof ActionDenied) {
    console.log(`Action denied: ${error.reason}`);
    // Handle denial
  }
  throw error;
}
```

## Configuration

### Constructor Options

```typescript
const client = new HaltStateClient({
  // Required
  tenantId: 'your-tenant-id',      // Your HaltState tenant identifier
  apiKey: 'hs_your_api_key',       // API key (prefix: hs_)

  // Optional
  baseUrl: 'https://haltstate.ai/ops',  // API endpoint (default)
  timeout: 30,                      // Request timeout in seconds (default: 30)
  failOpen: false,                  // Allow actions when API unreachable (default: false)
  retryCount: 3,                    // Retry attempts for transient errors (default: 3)
  agentId: 'my-agent',             // Identifier for this agent instance
  enableKillSwitch: false,          // Enable background heartbeat monitoring
  killSwitchInterval: 30,           // Heartbeat interval in seconds (default: 30)
});
```

### Environment Variables

The SDK reads these environment variables as fallbacks:

```bash
HALTSTATE_TENANT_ID=your-tenant-id
HALTSTATE_API_KEY=hs_your_api_key
HALTSTATE_BASE_URL=https://haltstate.ai/ops
HALTSTATE_AGENT_ID=my-agent
```

## Core Methods

### check(action, params?)

Check if an action is allowed without blocking. Returns a `CheckResult`.

```typescript
const result = await client.check('delete_database', {
  database: 'production',
  reason: 'cleanup'
});

if (result.decision === Decision.ALLOW) {
  // Safe to proceed
} else if (result.decision === Decision.APPROVAL_REQUIRED) {
  // Need human approval - result.approvalId contains the request ID
} else {
  // Denied by policy
}
```

### guard(action, options?)

Guard an action with automatic approval handling. Throws control flow exceptions.

```typescript
import { ApprovalPending, ActionDenied, ActionExpired } from '@haltstate/sdk';

try {
  const permit = await client.guard('deploy_to_production', {
    params: { version: '2.0.0', environment: 'prod' },
    idempotencyKey: `deploy-${buildId}`,
    agentId: 'deploy-agent',
  });

  // Permit received - action is approved
  console.log(`Approved by ${permit.approver} at ${permit.approvedAt}`);

} catch (error) {
  if (error instanceof ApprovalPending) {
    // Action requires human approval - exit and retry later
    console.log(`Approval pending: ${error.approvalId}`);
    return;
  }
  if (error instanceof ActionDenied) {
    // Blocked by policy
    console.log(`Denied: ${error.reason} (policy: ${error.policyId})`);
    return;
  }
  if (error instanceof ActionExpired) {
    // Previous approval expired - need to re-request
    console.log(`Expired: ${error.idempotencyKey}`);
    return;
  }
  throw error;
}
```

### withGuard(action, fn, options?)

Execute a function only if the action is approved. Automatically reports the outcome.

```typescript
const result = await client.withGuard(
  'send_email_blast',
  async (permit) => {
    // This only runs if approved
    const sent = await sendEmails(campaignId);
    return { emailsSent: sent };
  },
  {
    params: { campaign: campaignId, recipients: 50000 },
    idempotencyKey: `campaign-${campaignId}`,
  }
);
```

### waitForApproval(approvalId, options?)

Block until an approval is decided (approved/rejected/expired).

```typescript
try {
  const decision = await client.waitForApproval(approvalId, {
    timeout: 300,        // 5 minutes
    pollInterval: 2,     // Start with 2s polling
    maxPollInterval: 10, // Max 10s between polls
  });

  if (decision.status === ApprovalStatus.APPROVED) {
    console.log(`Approved by ${decision.approver}`);
  }
} catch (error) {
  if (error instanceof HaltStateApprovalTimeoutError) {
    console.log('Approval not received within timeout');
  }
}
```

### report(permit, options?)

Report the outcome of an approved action for audit trail.

```typescript
await client.report(permit, {
  result: {
    success: true,
    recordsProcessed: 1500,
    duration_ms: 3200,
  },
});
```

### onApproval(callback, options?)

Subscribe to real-time approval decisions via SSE.

```typescript
const unsubscribe = client.onApproval(
  (decision) => {
    console.log(`Decision for ${decision.requestId}: ${decision.status}`);
    if (decision.status === ApprovalStatus.APPROVED) {
      // Resume the pending action
      resumeAction(decision.requestId);
    }
  },
  { catchUpHours: 1 } // Also receive decisions from last hour
);

// Later: stop listening
unsubscribe();
```

### getApprovalsSince(hours)

Get historical approval decisions.

```typescript
const decisions = await client.getApprovalsSince(24); // Last 24 hours
for (const decision of decisions) {
  console.log(`${decision.action}: ${decision.status}`);
}
```

### close()

Clean up resources (stop heartbeat, close SSE connections).

```typescript
client.close();
```

## Error Handling

### Exception Hierarchy

```
HaltStateError (base)
├── HaltStateConnectionError  - Network/connection failures
├── HaltStateAuthError        - Invalid API key (401)
├── HaltStateRateLimitError   - Rate limit exceeded (429)
├── HaltStateApprovalTimeoutError - waitForApproval() timeout
├── ApprovalPending           - Action awaiting human approval (not an error)
├── ActionDenied              - Action blocked by policy
├── ActionExpired             - Previous approval expired
└── KillSwitchTriggered       - Remote kill switch activated
```

### Type Guards

```typescript
import { isControlFlowSignal, isHaltStateError } from '@haltstate/sdk';

try {
  await client.guard('action');
} catch (error) {
  if (isControlFlowSignal(error)) {
    // ApprovalPending, ActionDenied, or ActionExpired
    // These are expected control flow, not errors
    return;
  }
  if (isHaltStateError(error)) {
    // HaltState-specific error
    console.error('HaltState error:', error.message);
  }
  throw error;
}
```

## Kill Switch

Enable the kill switch to allow remote termination of your agent:

```typescript
const client = new HaltStateClient({
  tenantId: 'your-tenant',
  apiKey: 'hs_key',
  agentId: 'critical-agent-01',
  enableKillSwitch: true,
  killSwitchInterval: 30, // Check every 30 seconds
});

// The client will automatically throw KillSwitchTriggered
// if a kill signal is received from the HaltState dashboard
```

## Examples

### Cron Job Pattern

```typescript
// payment-processor.ts
import { HaltStateClient, ApprovalPending, ActionDenied } from '@haltstate/sdk';

async function processScheduledPayments() {
  const client = new HaltStateClient({
    tenantId: process.env.HALTSTATE_TENANT_ID!,
    apiKey: process.env.HALTSTATE_API_KEY!,
    agentId: 'payment-cron',
  });

  const payments = await getScheduledPayments();

  for (const payment of payments) {
    try {
      const permit = await client.guard('process_payment', {
        params: { amount: payment.amount, recipient: payment.to },
        idempotencyKey: `payment-${payment.id}`,
      });

      await executePayment(payment);
      await client.report(permit, { success: true });

    } catch (error) {
      if (error instanceof ApprovalPending) {
        // Will retry on next cron run
        console.log(`Payment ${payment.id} awaiting approval`);
        continue;
      }
      if (error instanceof ActionDenied) {
        await markPaymentDenied(payment.id, error.reason);
        continue;
      }
      throw error;
    }
  }

  client.close();
}
```

### Real-time Approval Handler

```typescript
// approval-handler.ts
import { HaltStateClient, ApprovalStatus } from '@haltstate/sdk';

const client = new HaltStateClient({
  tenantId: process.env.HALTSTATE_TENANT_ID!,
  apiKey: process.env.HALTSTATE_API_KEY!,
  agentId: 'approval-handler',
});

// Listen for approval decisions and resume pending work
client.onApproval(async (decision) => {
  if (decision.status === ApprovalStatus.APPROVED) {
    const pendingWork = await getPendingWork(decision.requestId);
    if (pendingWork) {
      await executePendingWork(pendingWork);
    }
  }
}, { catchUpHours: 1 });

// Keep process alive
process.on('SIGTERM', () => client.close());
```

## TypeScript Support

This package includes full TypeScript definitions. All types are exported:

```typescript
import type {
  HaltStateConfig,
  CheckResult,
  Permit,
  ApprovalDecision,
  GuardOptions,
  WaitOptions,
} from '@haltstate/sdk';
```

## License

MIT