npm package: @gizmo-ai/hitl-plugin
Description: Human-in-the-Loop approval plugin for Gizmo runtime
Latest version: 0.2.1

# @gizmo-ai/hitl-plugin

Human-in-the-Loop approval plugin for Gizmo. Blocks tool calls pending human approval via HTTP API.

## Install

```bash
bun add @gizmo-ai/hitl-plugin
```

## Quick Start

```typescript
import { createRuntime } from "@gizmo-ai/runtime";
import { agentPlugin } from "@gizmo-ai/agent-plugin";
import { createServer, createServerPlugin } from "@gizmo-ai/server";
import { createHITLPlugin } from "@gizmo-ai/hitl-plugin";

// 1. Create HITL plugin
const hitl = createHITLPlugin({
  requiresApproval: ["bash", "write", "edit"],
  defaultTimeoutMs: 60000,
});

// 2. Create runtime with HITL plugin FIRST
const agent = agentPlugin({ model, tools });
const serverPlugin = createServerPlugin();

const runtime = createRuntime({
  plugins: [hitl.plugin, agent, serverPlugin.plugin],
});

// 3. Connect HITL to runtime
hitl.connect(runtime);

// 4. Create server and wire HITL routes
const server = createServer({ runtime, plugin: serverPlugin });
server.app.route("/approvals", hitl.createRoutes());
server.listen(3000);
```

## Configuration

```typescript
interface HITLPluginConfig {
  // Which tools require approval
  requiresApproval: "all" | string[] | ((toolName: string, args: Record<string, unknown>) => boolean);

  // Timeout for approval requests (0 = no timeout)
  defaultTimeoutMs?: number;  // Default: 60000

  // History ring buffer size
  historySize?: number;       // Default: 100
}
```

## HTTP API

| Method | Path | Description |
|--------|------|-------------|
| GET | `/approvals` | List pending approvals |
| GET | `/approvals/:id` | Get approval details |
| POST | `/approvals/:id` | Approve or reject |
| GET | `/approvals/history` | Recent decisions |

### Approve/Reject

```bash
# Approve
curl -X POST http://localhost:3000/approvals/<id> \
  -H "Content-Type: application/json" \
  -d '{"decision": "approve", "reason": "Looks safe"}'

# Reject
curl -X POST http://localhost:3000/approvals/<id> \
  -H "Content-Type: application/json" \
  -d '{"decision": "reject", "reason": "Dangerous command"}'
```

## Programmatic API

```typescript
// Get pending approvals
const pending = hitl.getPending();

// Approve programmatically
hitl.approve(approvalId, "Auto-approved by policy");

// Reject programmatically
hitl.reject(approvalId, "Auto-rejected by policy");
```

## Custom Approval Logic

```typescript
const hitl = createHITLPlugin({
  requiresApproval: (toolName, args) => {
    // Only require approval for destructive bash commands
    if (toolName === "bash") {
      const cmd = args.command as string;
      return cmd.includes("rm") || cmd.includes("delete");
    }
    return false;
  },
});
```

## Actions

| Action | Description |
|--------|-------------|
| `HITL_APPROVAL_REQUESTED` | Tool blocked, awaiting decision |
| `HITL_APPROVAL_GRANTED` | Human approved, tool replayed |
| `HITL_APPROVAL_REJECTED` | Human rejected, tool fails |
| `HITL_APPROVAL_EXPIRED` | Timeout reached, tool fails |

## State

```typescript
interface HITLState {
  pending: Record<string, PendingApproval>;  // By approval ID
  history: PendingApproval[];                // Ring buffer
  config: { defaultTimeoutMs: number; historySize: number };
}

interface PendingApproval {
  id: string;
  executionId: string;
  toolCall: { id: string; name: string; args: Record<string, unknown> };
  status: "pending" | "approved" | "rejected" | "expired";
  requestedAt: number;
  expiresAt?: number;
  decidedAt?: number;
  reason?: string;
}
```

## Security

- Approval IDs are UUIDs (`crypto.randomUUID()`)
- State validated before approval (must be pending + have blocked action)
- Expired approvals cannot be approved (410 Gone)
- Re-approving returns 409 Conflict

## License

MIT