Skip to main content
Open ProtocolAdapters

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

  1. Your adapter intercepts an AI tool call (before or after completion)
  2. It extracts metadata: model, tokens, latency, cost, error code
  3. It POSTs a signed JSON payload to the local FORG agent
  4. The agent validates the signature, evaluates policies, and returns a response
  5. 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": "<current>" }

# 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 --adapter

Session lifecycle

The FORG agent manages session IDs for you if you don't provide one:

  • If session_id is omitted, the agent uses the current active session for this user
  • A new session is started after session_timeout seconds of inactivity (default: 1800s)
  • Sending a SessionEnd hook 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.json manifest with name, version, and supported hooks
  • Tests that verify signal shape and HMAC authentication
  • Fail-open behavior when the FORG agent is unreachable
© 2026 FORG by UpgradIQ, Inc. All rights reserved.Edit this page on GitHub