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 rules, 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": "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 --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 UpgradIQ, Inc.Edit this page on GitHub