Build an Adapter
The FORG adapter protocol is open. Any tool that can make HTTP requests can emit signals to the local agent. No SDK required — all you need is a POST to http://127.0.0.1:6247/emit with a JSON payload and an HMAC-SHA256 signature.
Protocol overview
- Your adapter intercepts an AI tool call (before or after completion)
- It extracts metadata: model, tokens, latency, cost, error code
- It POSTs a signed JSON payload to the local FORG agent
- The agent validates the signature, evaluates rules, and returns a response
- If the response is
blocked: true, your adapter should abort the tool call
Signal payload schema
// TypeScript type definition
interface ForgSignal {
// Required
adapter: string; // Unique adapter identifier, e.g. "my-tool-forg"
ts: string; // ISO 8601 timestamp of the event
model: string; // Model identifier, e.g. "claude-opus-4-5"
// Token & cost data (at least one required)
tokens_in?: number; // Input / prompt tokens
tokens_out?: number; // Output / completion tokens
cost_usd?: number; // Estimated cost in USD
// Optional context
latency_ms?: number; // Time from request to first byte (ms)
session_id?: string; // Carry forward from SessionStart response
project_id?: string; // Project context
user_id?: string; // Developer identity (default: license user)
error_code?: string | null; // null if successful; error string if failed
// Optional: hook type for multi-hook setups
hook?: "PostToolUse" | "SessionStart" | "SessionEnd" | "Stop";
}HMAC-SHA256 authentication
Every emit request must include an X-Forg-Signature header with an HMAC-SHA256 of the request body, signed with the session key:
X-Forg-Signature: sha256=<hex-digest>The session key is available in the FORG_SESSION_KEY environment variable, which the FORG agent sets when launching hook processes. For standalone adapters that are not launched by the agent, request a session key first:
POST http://127.0.0.1:6247/session/start
Content-Type: application/json
{
"adapter": "my-tool-forg",
"user_id": "user_abc123" // optional
}
// Response:
{
"session_id": "sess_4f9a2e1b8c3d",
"session_key": "base64-encoded-32-byte-key",
"expires_at": "2025-05-28T12:00:00Z"
}Full TypeScript adapter example
import crypto from "crypto";
import { readFileSync } from "fs";
const AGENT_URL = "http://127.0.0.1:6247";
interface SessionState {
sessionId: string;
sessionKey: string;
}
class ForgAdapter {
private state: SessionState | null = null;
private readonly adapterName: string;
constructor(adapterName: string) {
this.adapterName = adapterName;
}
/** Start a FORG session. Call once when your tool session begins. */
async startSession(userId?: string): Promise<void> {
const res = await fetch(`${AGENT_URL}/session/start`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ adapter: this.adapterName, user_id: userId }),
});
if (!res.ok) {
// Fail open — FORG unavailable should never block the tool
console.warn(`[forg] session start failed: ${res.status}`);
return;
}
const { session_id, session_key } = await res.json();
this.state = { sessionId: session_id, sessionKey: session_key };
console.log(`[forg] session started: ${session_id}`);
}
/** Emit a signal after an AI model call. Returns true if blocked. */
async emit(signal: {
model: string;
tokensIn: number;
tokensOut: number;
latencyMs: number;
costUsd?: number;
errorCode?: string | null;
projectId?: string;
}): Promise<{ blocked: boolean; message?: string }> {
if (!this.state) {
// Start session lazily if not started
await this.startSession();
if (!this.state) return { blocked: false };
}
const payload: Record<string, unknown> = {
adapter: this.adapterName,
ts: new Date().toISOString(),
model: signal.model,
tokens_in: signal.tokensIn,
tokens_out: signal.tokensOut,
latency_ms: signal.latencyMs,
cost_usd: signal.costUsd ?? null,
error_code: signal.errorCode ?? null,
session_id: this.state.sessionId,
project_id: signal.projectId ?? null,
};
const body = JSON.stringify(payload);
const signature = this.sign(body, this.state.sessionKey);
try {
const res = await fetch(`${AGENT_URL}/emit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Forg-Signature": `sha256=${signature}`,
},
body,
signal: AbortSignal.timeout(3000), // respect emit_timeout
});
if (!res.ok) {
console.warn(`[forg] emit failed: ${res.status}`);
return { blocked: false };
}
const result = await res.json();
return {
blocked: result.blocked === true,
message: result.message,
};
} catch (err) {
// Fail open on timeout or connection refused
console.warn("[forg] emit error (fail open):", (err as Error).message);
return { blocked: false };
}
}
/** End the current FORG session. */
async endSession(): Promise<void> {
if (!this.state) return;
const body = JSON.stringify({
adapter: this.adapterName,
session_id: this.state.sessionId,
hook: "SessionEnd",
ts: new Date().toISOString(),
});
const signature = this.sign(body, this.state.sessionKey);
await fetch(`${AGENT_URL}/emit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Forg-Signature": `sha256=${signature}`,
},
body,
}).catch(() => {}); // best-effort
this.state = null;
}
private sign(body: string, key: string): string {
const keyBuf = Buffer.from(key, "base64");
return crypto.createHmac("sha256", keyBuf).update(body, "utf8").digest("hex");
}
}
// ── Usage example ──────────────────────────────────────────────
const forg = new ForgAdapter("my-custom-tool");
async function runAiCall(prompt: string): Promise<string> {
await forg.startSession();
const start = Date.now();
// Your AI call here...
const response = await myAiClient.complete(prompt);
const latencyMs = Date.now() - start;
const { blocked, message } = await forg.emit({
model: response.model,
tokensIn: response.usage.input_tokens,
tokensOut: response.usage.output_tokens,
latencyMs,
costUsd: response.usage.cost_usd,
errorCode: response.error ?? null,
});
if (blocked) {
throw new Error(`FORG policy blocked this request: ${message}`);
}
return response.text;
}
process.on("exit", () => { forg.endSession(); });Testing your adapter
# Check agent is reachable:
curl http://127.0.0.1:6247/health
# { "status": "ok", "version": "1.4.2" }
# Send a test signal (no auth required for test signals):
forg emit '{
"adapter": "test",
"model": "claude-opus-4-5",
"tokens_in": 100,
"tokens_out": 50,
"latency_ms": 500,
"ts": "2025-05-28T10:00:00Z"
}'
# Check it appeared:
forg status --adapterSession lifecycle
The FORG agent manages session IDs for you if you don't provide one:
- If
session_idis omitted, the agent uses the current active session for this user - A new session is started after
session_timeoutseconds of inactivity (default: 1800s) - Sending a
SessionEndhook closes the session immediately
Submitting to the community registry
Built an adapter for a tool not yet covered? Submit a pull request to the forg-adapters repository. Community adapters are reviewed for the open protocol compliance and listed in the FORG documentation.
Your adapter should include:
- A README with prerequisites, installation, and configuration steps
- A
forg-adapter.jsonmanifest with name, version, and supported hooks - Tests that verify signal shape and HMAC authentication
- Fail-open behavior when the FORG agent is unreachable