Governance gateway example (TypeScript)

Runs one of your own agents under ThreadPool's authority, with no dependencies (Node.js 18 or later): it opens a governed run, declares a step, asks for authority, runs the agent only if allowed, reports the result, has ThreadPool verify it and closes the run.

Save it as gateway.ts and run it with tsx, using a key with the gateway:run scope:

bash
export THREADPOOL_URL="https://threadpool.example.com"
export THREADPOOL_API_KEY="tpk_..."

npx tsx gateway.ts "Summarise our Q3 support tickets"

The model, endpoints and decisions are described in Governance gateway.

Source

ts
// ThreadPool governance gateway: run one of YOUR agents under ThreadPool's
// authority, with no dependencies (Node 18+ for global fetch). Run it with tsx:
//
//   export THREADPOOL_URL=https://threadpool.example.com   # or http://localhost:3000
//   export THREADPOOL_API_KEY=tpk_...                      # scope: gateway:run
//
//   npx tsx docs/api/examples/gateway.ts "Summarise our Q3 support tickets"
//
// ThreadPool never runs your agent: it decides whether the step may run,
// verifies what the agent produced and records who did what. See
// docs/api/gateway.md.

import { randomUUID } from "node:crypto";

const BASE_URL = (process.env.THREADPOOL_URL ?? "http://localhost:3000").replace(/\/$/, "");
const API_KEY = process.env.THREADPOOL_API_KEY ?? "";

// ── Minimal client ──────────────────────────────────────────────────────

class GatewayError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    message: string,
    readonly details?: Record<string, unknown>,
  ) {
    super(`${status} ${code}: ${message}`);
  }
}

// Every gateway write carries an Idempotency-Key, reused if the call is retried.
async function api<T>(method: "GET" | "POST", path: string, body?: unknown, idempotencyKey = randomUUID()): Promise<T> {
  const res = await fetch(`${BASE_URL}${path}`, {
    method,
    headers: {
      authorization: `Bearer ${API_KEY}`,
      ...(method === "POST" ? { "content-type": "application/json", "idempotency-key": `example-${idempotencyKey}` } : {}),
    },
    body: method === "POST" ? JSON.stringify(body ?? {}) : undefined,
  });
  const data = await res.json();
  if (!res.ok) throw new GatewayError(res.status, data.error?.code ?? "UNKNOWN", data.error?.message ?? "request failed", data.error?.details);
  return data as T;
}

interface Step {
  id: string;
  sequence: number;
  status: string;
}

type Decision =
  | { decision: "allow"; credential: { id: string; expiresAt: string; usesRemaining: number } }
  | { decision: "deny"; reason: { code: string; message: string }; agentRevoked: boolean }
  | { decision: "pending_approval"; reason: { message: string }; approval: { id: string; expiresAt: string } };

interface Verdict {
  verdict: { passed: boolean; accepted: boolean; score: number; reason: string; issues: string[]; verifiedBy: string; verifiedByLabel: string; outputHash: string };
  outcome: "accepted" | "rejected";
  attemptsRemaining: number;
}

// ── Your agent (stands in for LangGraph, CrewAI, your own code...) ──────

async function myAgent(brief: string): Promise<string> {
  // Your agent runs here, with your models and your keys. ThreadPool never sees them.
  return [
    `## ${brief}`,
    "",
    "Ticket volume rose 12% quarter on quarter, driven by billing questions after the September pricing change.",
    "Login issues fell by a third once single sign-on shipped in August.",
    "Recommended next step: add a billing FAQ to the checkout page and track whether billing tickets drop in Q4.",
  ].join("\n");
}

// ── The governed flow ───────────────────────────────────────────────────

async function main() {
  const brief = process.argv[2] ?? "Summarise our Q3 support tickets";
  if (!API_KEY) throw new Error("Set THREADPOOL_API_KEY (a key with the gateway:run scope).");

  const { run } = await api<{ run: { id: string } }>("POST", "/api/v1/gateway/runs", { title: brief, qualityThreshold: 70 });
  console.log(`run ${run.id} opened`);

  const { step } = await api<{ step: Step }>("POST", `/api/v1/gateway/runs/${run.id}/steps`, {
    capability: "summarization",
    description: brief,
    agent: { id: "example-summariser-v1", name: "Example summariser" },
  });

  // 1. Ask before acting. Act ONLY on "allow".
  const decision = await api<Decision>("POST", `/api/v1/gateway/runs/${run.id}/steps/${step.id}/authorize`);
  if (decision.decision === "pending_approval") {
    console.log(`waiting for a human: approval ${decision.approval.id} (expires ${decision.approval.expiresAt}). Ask again after it is decided.`);
    return;
  }
  if (decision.decision === "deny") {
    console.log(`refused (${decision.reason.code}): ${decision.reason.message}`);
    return;
  }
  console.log(`authorized: credential ${decision.credential.id}, expires ${decision.credential.expiresAt}`);

  // 2. Your agent does the work, outside ThreadPool.
  const started = Date.now();
  const output = await myAgent(brief);

  // 3. Report under that credential. ThreadPool records the output's SHA-256.
  const reported = await api<{ report: { outputHash: string; measuredLatencyMs: number } }>("POST", `/api/v1/gateway/runs/${run.id}/steps/${step.id}/report`, {
    credentialId: decision.credential.id,
    output,
    latencyMs: Date.now() - started,
  });
  console.log(`reported: sha256 ${reported.report.outputHash}`);

  // 4. ThreadPool verifies it independently and accepts or rejects the step.
  const verified = await api<Verdict>("POST", `/api/v1/gateway/runs/${run.id}/steps/${step.id}/verify`);
  console.log(`${verified.outcome}: score ${verified.verdict.score} - ${verified.verdict.verifiedByLabel}`);
  if (verified.outcome === "rejected") console.log(`issues: ${verified.verdict.issues.join("; ") || verified.verdict.reason}`);

  // 5. Close: the final accountability record and the governance receipt.
  const closed = await api<{ summary: Record<string, number>; receipt: { hash: string } }>("POST", `/api/v1/gateway/runs/${run.id}/close`);
  console.log("closed:", closed.summary, `receipt sha256 ${closed.receipt.hash}`);
}

main().catch((err) => {
  console.error(err instanceof GatewayError ? `${err.message}${err.details ? ` ${JSON.stringify(err.details)}` : ""}` : err);
  process.exit(1);
});