TrueForge
Integrate Intutic governance with TrueForge (TrueFoundry, MIT) — an open-source agent-runtime library. Intutic supports both of TrueForge's deployment modes, each with its own HarnessType and its own gate mechanism:
- Embedded (
HarnessType.TRUEFORGE) — another team's Node.js process imports@truefoundry/trueforge-coredirectly. Covered below in Embedded mode. - Standalone/hosted server (
HarnessType.TRUEFORGE_SERVER) — TrueForge runs as its own server (npx @truefoundry/trueforge, Docker Compose, or the Helm chart). Covered below in Server mode (standalone/hosted).
Both modes share the same underlying approval mechanism — TrueForge's async tool.approval_required/user.tool_approval turn contract — but differ in WHERE the gate runs: SDK-side in your own process for embedded mode, or out-of-process in the TrueForge bridge (a separate service you run) for server mode, since nobody embeds a gate library into a third-party OSS server process. See harness-security-matrix.md rows 41–42 for how these two trust shapes are scored differently.
Embedded mode
TrueForge (embedded) is governed on two independent surfaces:
- LLM egress — point TrueForge's model-provider config (
baseUrl) at the Intutic proxy. Every LLM call crosses the proxy and is governed like any other harness. - Tool-call approval — TrueForge pauses a turn with a
tool.approval_requiredevent before running a tool yourrequire_approval_for_toolsselector covers.@intutic/gate/trueforge'sintuticApprovalResponder()answers that pause with a realGate.guard()verdict.
How it works
The trueforge adapter is detected when package.json declares an @truefoundry/trueforge-core dependency (any version). Like every other SDK-gated framework in this codebase, it writes a .env.intutic file with proxy base-URL env vars plus a comment block pointing at the SDK gate.
Important, confirmed against a real install (@truefoundry/trueforge-core@0.1.4): unlike Mastra's beforeToolCall or the Vercel AI SDK's toolApproval, TrueForge has no synchronous, in-process approval callback you register once and forget. The only approval mechanism it exposes — embedded or run as a standalone server — is an async turn/event contract: a turn pauses with a tool.approval_required required action, and you answer it by starting a new turn carrying a user.tool_approval input item. intuticApprovalResponder() is the piece that evaluates pending requests through the gate; driving the turn/event loop itself is your own application code, not something this adapter turns on for you. See What the adapter does NOT do.
Setup
1. Initialize Intutic
intutic init ✔ trueforge → /path/to/project/.env.intuticintutic init only detects the harness and records it in ~/.intutic/config.json; it writes no harness files. The files described on this page are written by intutic gates install (open core, no control plane), from the rule sets in .intutic/sops/ — see What writes harness files:
intutic gates install2. Route LLM traffic through the proxy
TrueForge's core LLM layer is built on the Vercel AI SDK's provider-construction shape (VercelAIProviderConfig, with an explicit baseUrl field) — there is no environment variable it reads automatically. Set baseUrl when you construct the model provider config:
const providerConfig = {
provider: { type: 'openai', name: 'my-openai' },
model: { id: 'gpt-4o', name: 'gpt-4o' },
name: 'gpt-4o',
baseUrl: `${process.env.INTUTIC_PROXY_URL}/v1`, // OpenAI-style providers need the /v1 path
apiKey: process.env.OPENAI_API_KEY!,
headers: {},
}If your embedding host exposes TrueForge's own settings API (PUT /api/v1/settings/model-providers), point that at the proxy instead — same effect, different call site.
3. Gate tool-call approval (SDK)
npm install @intutic/gateimport { Gate, GateClient, install } from '@intutic/gate'
import { intuticApprovalResponder } from '@intutic/gate/trueforge'
// The client reads INTUTIC_API_KEY / INTUTIC_WORKSPACE_ID, or the credentials a
// control-plane login saved. Without it the gate still enforces the local policy
// snapshot, but skips your SOP rules, the control-plane check and audit events.
const client = GateClient.fromEnv({ sessionId: runId, harness: 'trueforge' })
install(new Gate({ workspaceId: client.workspaceId }, client))
const respond = intuticApprovalResponder()
// Your own turn-driving loop (illustrative — see the caveats below):
for await (const event of turn.stream()) {
// ... handle other event types ...
}
// When the turn's stored record shows a pending `tool.approval_required`
// required action, resolve each `{id, source_event_id}` back to the real
// tool name + arguments yourself (e.g. via turn.listEvents()), then:
const pendingRequests = pendingApprovals.map((p) => ({
threadId: p.threadId,
toolCallId: p.id,
toolName: resolvedToolName(p),
input: resolvedToolInput(p),
}))
const input = await respond(pendingRequests)
const nextTurn = await session.createTurn({
turn_id: newTurnId(),
input,
previous_turn_id: pausedTurn.id,
signal,
resolver,
})Decision mapping per pending request:
- gate allows →
{ status: 'allow' } - gate denies →
{ status: 'deny', reason }— the[Intutic Governance] BLOCKED: ...message - the gate itself crashes →
{ status: 'deny', reason: '... gate crashed ...' }— fails closed, so a broken gate never silently lets a call through
What gets written
Same shape as every other SDK-gated framework's .env.intutic — proxy URLs plus a pointer at @intutic/gate/trueforge's intuticApprovalResponder():
# Intutic Governance Rules (auto-generated)
# DO NOT EDIT — managed by intutic sync daemon
# Last sync: 2026-08-29T00:00:00Z
# Source this file: source .env.intutic
export ANTHROPIC_BASE_URL="http://localhost:4000"
export OPENAI_BASE_URL="http://localhost:4000/v1"
export INTUTIC_PROXY_URL="http://localhost:4000"
export INTUTIC_SOP_COUNT=5
# These env vars govern LLM egress only. TrueForge (embedded) tools run in
# your own Node.js process, where no config or hook file can gate them — the
# blocking tool gate ships SDK-side:
# npm install @intutic/gate
# import { intuticApprovalResponder } from '@intutic/gate/trueforge'
# intuticApprovalResponder() answers tool.approval_required pauses with a
# real Gate.guard() verdict, producing user.tool_approval items for your
# next session.createTurn() call.Note the env vars above are written for consistency with every other harness's .env.intutic, but — like Mastra and the Vercel AI SDK — TrueForge's core LLM layer does not read OPENAI_BASE_URL/ANTHROPIC_BASE_URL automatically; you still have to pass baseUrl into the provider config yourself (step 2 above).
What the adapter does NOT do
Stated plainly, because the gaps are structural:
- This section covers embedded mode only. TrueForge run as its own standalone or hosted server (
npx @truefoundry/trueforge, Docker Compose, or the Helm chart) is a differentHarnessTypewith a different gate mechanism — see Server mode (standalone/hosted) below, not this section. - There is no sync-daemon hook file. Every hook-based harness has an on-disk config the daemon writes a pre-tool gate into; TrueForge (embedded) has none — tools run inside your own process. The daemon cannot gate what it cannot reach from disk.
- TrueForge has no synchronous approval callback. Confirmed against a real install: there is nothing shaped like Mastra's
beforeToolCallor the Vercel AI SDK'stoolApprovalanywhere in@truefoundry/trueforge-core.intuticApprovalResponder()only evaluates the pending requests you hand it — building and driving the turn/event loop that detects a pause, resolves the real tool name and arguments from the referencedmodel.messageevent, and starts the next turn with the answer is your own application code, not something this adapter provides or verifies for you. - Coverage depends on your
require_approval_for_toolsselector. TrueForge's own MCP tool-selector policy (@all/@write/@destructive/ a specific tool name) controls which tool calls even emit atool.approval_requiredpause in the first place — a narrower selector means some tool calls never pause at all, and this gate never sees them. This is TrueForge's own default-scoped design, not an Intutic gap, but it means the gate's real coverage is exactly as broad as the selector you configure. - Attribution is client-supplied. There is no
x-intutic-harnesswiring specific to this adapter yet; scope your proxy client headers the same way you would for any other in-code provider construction.
Config details
| Property | Value |
|---|---|
| Harness type | trueforge |
| Config file | .env.intutic |
| Detection | @truefoundry/trueforge-core in package.json (dependencies, devDependencies, or peerDependencies) |
| Format | Shell environment variables |
| Write strategy | Atomic (write to .intutic-tmp, then rename) |
| Tool gate | SDK-side (@intutic/gate/trueforge's intuticApprovalResponder(), TrueForge's tool.approval_required/user.tool_approval turn contract) — no sync-daemon hook file, no synchronous callback; embedded-library mode only |
Server mode (standalone/hosted)
TrueForge run as its own standalone or hosted server — npx @truefoundry/trueforge, its Docker Compose stack, or its Helm chart — is a separate process nobody embeds a gate library into. HarnessType.TRUEFORGE_SERVER governs it via an out-of-process gate service, the TrueForge bridge, on the same two surfaces as embedded mode:
- LLM egress — same mechanism as embedded mode, configured once at the server level instead of per embedding host:
PUT /api/v1/settings/model-providerswithbaseUrlpointed at the Intutic proxy. - Tool-call approval —
services/trueforge-bridgesubscribes to each watched session's turn stream, and when a turn pauses withtool.approval_required, resolves the pending call, evaluates it, and answers with a real verdict — the same governance decision embedded mode'sGate.guard()would produce, reached a different way (see How it works).
How it works
Unlike every other harness in this catalog, TRUEFORGE_SERVER is not detected by intutic init's repo scan — there is no package.json dependency or config file to find, because this is an operator-configured deployment, not a library your repo depends on. There is no writeConfig step and no .env.intutic for this row. Instead, an operator runs the TrueForge bridge (its own Docker image) and points it at their running TrueForge server with environment variables.
Confirmed against the real TrueForge source (packages/trueforge-core/src/core/events/schema.ts, packages/trueforge/src/routes/turnRoutes.ts), not assumed: the standalone server exposes the exact same async turn/event contract the embedded package does — a turn pauses with a tool.approval_required event naming each pending call only as {id, source_event_id} (all fields snake_case on the actual wire JSON; the published @truefoundry/trueforge-sdk TS client's camelCase property names, e.g. sourceEventId, are a client-side naming convenience its generator applies on top of that same snake_case JSON — a real, worth-stating distinction since services/trueforge-bridge speaks plain HTTP directly rather than depending on that SDK package). There is no webhook — TrueForge never pushes anything to an external system, so the bridge must hold a subscription to the turn stream; it cannot be notified.
The bridge:
- Watches each configured session's current turn over SSE (
GET /{session_id}/turns/{turn_id}/subscribe), reconnecting afterTRUEFORGE_RECONNECT_DELAY_MSwhen the stream drops. There is no polling mode. - On
tool.approval_required, resolves each pending call'ssource_event_idback to the real tool name and arguments via the referencedmodel.messageevent'stool_calls[]. - Evaluates the call through the same
packages/gate-jsmachinery embedded mode uses —soprules.ts's SOP argPattern rules, thenPOST /api/v1/hook-gate— fail-closed on any transport/parse error, matching every other gate-js adapter's convention. - Resumes the turn:
POST /{session_id}/turnswithprevious_turn_idchained to the paused turn and auser.tool_approvalinput item carrying the verdict.
Setup
1. Deploy the bridge
The bridge is not part of the open-core repository. In a full Intutic source checkout it is built from the monorepo root:
docker build -f services/trueforge-bridge/Dockerfile -t intutic-trueforge-bridge .There is no Helm value or ready-to-apply Kubernetes manifest for it: run the image yourself (one replica, restart on exit — it is a background worker with no port) with the environment below.
2. Configure the bridge
| Var | Required | Description |
|---|---|---|
TRUEFORGE_BASE_URL | yes | Base URL of your TrueForge server. |
INTUTIC_API_KEY | yes | Intutic workspace API key — used for the POST /api/v1/hook-gate call. |
INTUTIC_WORKSPACE_ID | yes | Intutic workspace id. |
TRUEFORGE_API_KEY | no | Bearer token for the TrueForge server, if its own OIDC is configured. |
INTUTIC_CONTROL_PLANE_URL | no | Defaults to Intutic's hosted control plane; set explicitly for a self-hosted deployment. |
TRUEFORGE_SESSION_IDS | no | Comma-separated session ids to watch. Empty means: discover via GET /api/v1/sessions at startup. |
TRUEFORGE_RECONNECT_DELAY_MS | no | Wait before re-subscribing after a turn stream drops. Default 2000. |
The bridge refuses to start if TRUEFORGE_BASE_URL, INTUTIC_API_KEY, or INTUTIC_WORKSPACE_ID is missing — an unconfigured bridge that starts anyway and silently governs nothing would be worse than one that fails loudly at boot.
3. Route LLM traffic through the proxy
curl -X PUT "$TRUEFORGE_BASE_URL/api/v1/settings/model-providers" \
-H "Content-Type: application/json" \
-d '{"baseUrl": "'"$INTUTIC_PROXY_URL/v1"'", ...}'Same mechanical routing as embedded mode (step 2 there) — TrueForge's LLM layer reads no env var for this, so the base URL has to be set explicitly, now at the server level rather than per embedding host.
4. Set your MCP server's approval selector to @all
This is the step embedded mode does not need an operator to think about (an embedding host wires its own approval loop), and it is the single most consequential setting for how much of your traffic this gate actually sees — see the caveat below.
What gets written
Nothing, on the TrueForge side — TRUEFORGE_SERVER writes no config file and has no instructions file (see How it works). What "gets written" instead is the bridge's own decision log: every allow/deny verdict is logged structurally (@intutic/logger). Every refusal is reported by the bridge as a tool_blocked hook event, so it is filed as an incident and shows up on the dashboard: one on a SOP BLOCK: or approval rule, one the bridge cannot resolve to a real call, one the control plane's POST /api/v1/hook-gate gives, and one made because the hook gate could not be reached. A hook-gate refusal's report carries the hook gate's decisionId as gateDecisionId, so the decision is filed once (the hook gate files its DLP refusals itself) and reaches a SIEM destination once, as the hook gate's own gate_decisions record. The report is best-effort: it never delays or changes the decision.
What the adapter does NOT do
Stated plainly, because the gaps are structural — and some of them are more consequential here than in embedded mode, since there is no embedding host controlling the wiring:
- Coverage depends entirely on your MCP server's approval-tool-selector. TrueForge's own per-MCP-server config (
McpServerApprovalToolSelector:"@all" | "@write" | "@destructive" | <tool name>) controls whether a tool call ever emitstool.approval_requiredin the first place. An operator who registers an MCP server without@all(or an equivalently broad selector) gets tool calls that never pause and that this bridge never sees at all — not a delayed or degraded check, an invisible one. This is TrueForge's own default-scoped design, not a bridge defect, but it is the load-bearing caveat of this entire integration: the bridge's real coverage is exactly as broad as the selector each MCP server is configured with. - This is a weaker trust boundary than every other Hook (A) mark in this catalog. Every other in-process or SDK-side gate this product ships runs co-resident with, or embedded in, the harness's own process. This gate runs in a wholly SEPARATE process (the TrueForge bridge) reacting to a pull-only event stream over the network — see
harness-security-matrix.mdrow 42, which marks this distinction explicitly rather than reusing the existing ✅/⚠️ symbols without comment. - There is no webhook. TrueForge never pushes anything to the bridge; the bridge must keep its SSE subscription alive. A bridge that is down, disconnected, or slow to reconnect misses pauses for that window — same operational risk as any other pull-based watcher, stated here rather than left implicit.
- Session discovery is intentionally simple. The bridge watches a configured list of session ids, or discovers sessions via
GET /api/v1/sessionsat startup — it does not dynamically track sessions created after startup unless re-deployed or re-configured to include them. - Attribution is the bridge's own, not the calling agent's. Verdicts are reported under
harnessType: 'trueforge-server'; there is no finer per-agent/per-thread attribution beyond what the resolved tool call and thread id already carry.
Config details
| Property | Value |
|---|---|
| Harness type | trueforge-server |
| Config file | none — not detected by intutic init; this is an operator-configured deployment |
| Detection | none (operator-configured; see HarnessType.TRUEFORGE_SERVER's doc comment in packages/shared-types/src/enums.ts) |
| Tool gate | Out-of-process (GateKind: 'bridge') — the TrueForge bridge, a service you run, watching TrueForge's tool.approval_required/user.tool_approval turn contract over HTTP/SSE; reuses packages/gate-js's soprules.ts and POST /api/v1/hook-gate, fail-closed |
| Deployment | Docker image built from services/trueforge-bridge/Dockerfile (full source checkout); no Helm value or ready-made manifest |