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);
});