Skip to main content
The Trust Fabric is Forge’s governance engine — 4,300+ lines of TypeScript across convex/trust_fabric/ that govern every agent action before execution is permitted. It evaluates two fail-fast gate pipelines (dispatch and launch), resolves role-based authority, and produces structured decisions with machine-readable explanations and unblock hints. This page is for engineers extending or debugging the governance system. For a higher-level overview of what each gate checks, see the Governance Pipeline flow.

Architecture Overview

The Trust Fabric follows a pure-core / impure-shell architecture. Gate functions are pure (no DB access, no side effects). Async operations like policy evaluation and concurrency counting are injected through a hooks interface. The Convex binding layer fetches data, wires hooks, and delegates to the engine.

TRUST FABRIC ARCHITECTURE

bindings.tsimpure shell
~850 loc
1. Fetch gateway, agent, budgets, role2. Resolve context via role_resolver3. Wire hooks as closures4. evaluateGovernance(ctx, hooks)
delegates to
engine.tspure orchestration
~1,150 loc

Runs gate sequence. Each gate: pure fn → SafetyGateResult → GateOutcome. Fail-fast on first block.

Dispatch PipelineLaunch PipelineHooks
invokes
gates/pure · testable
preflight
identity
safety
budget
context
policy
scope
autonomy
guardrails
two-agent
produces
Allow

All gates passed.

Block

Structured error + hints.

Hold

Approval required.

Key source files:
FileRoleLines
convex/trust_fabric/engine.tsGate orchestration, evaluation entry points~1,150
convex/trust_fabric/bindings.tsConvex action layer, data fetching, hook wiring~850
convex/trust_fabric/types.tsGovernanceDecision, GateOutcome, explanations, snapshots~420
convex/trust_fabric/role_resolver.tsRole, identity, authority, scope resolution~400
convex/trust_fabric/explanation.tsStructured explanation builder, unblock hints~640
convex/trust_fabric/gates/index.tsGate sequences, decision builders, shared types~430
convex/trust_fabric/gates/gate_*.tsIndividual gate implementations~430 total

Gate Anatomy

Every gate is a pure function that takes a GateContext (or a gate-specific input type) and returns a SafetyGateResult:
// convex/trust_fabric/gates/index.ts
export type SafetyGateResult =
  | { pass: true }
  | { pass: false; errorCode: string; retryable: boolean; message: string };
The GateContext is the shared input shape for most gates:
// convex/trust_fabric/gates/index.ts
export type GateContext = {
  agentDoc: AgentDoc;
  gatewayDoc: GatewayDoc;
  stepId: string;
  runId: string;
  budgetEnvelopes?: BudgetEnvelope[];
  resolvedBounds?: Partial<AuthorityBounds>;    // 050.6: role-derived limits
  trustedContextConfig?: TrustedContextConfig;   // 050.7: role trust requirements
  contextProvenance?: ContextProvenanceV1;       // 050.7: dispatch context provenance
  actionType?: ActionType;                       // STD-02.2: autonomy rung enforcement
};
Here is a representative gate — concurrencyGate from gate_3_safety.ts:
// convex/trust_fabric/gates/gate_3_safety.ts
export function concurrencyGate(ctx: GateContext, runningCount: number): SafetyGateResult {
  const max = ctx.resolvedBounds?.maxConcurrentSteps ?? ctx.agentDoc.maxConcurrentSteps ?? 1;
  if (runningCount >= max) {
    return {
      pass: false,
      errorCode: "agent_busy",
      retryable: true,
      message: `Agent '${ctx.agentDoc.agentId}' is at concurrency limit (${runningCount}/${max})`,
    };
  }
  return { pass: true };
}
Key patterns:
  • Pure function — no DB access, no side effects, trivially testable.
  • Resolved bounds first — gates prefer resolvedBounds (role-derived + agent overrides) over raw agentDoc fields. This makes role authority bounds actually enforce.
  • Fail with structure — failures return a machine-readable errorCode, a retryable flag, and a human-readable message. The engine uses all three.

Gate Composition

Two ordered sequences define which gates run for each action type. Both are fail-fast: the first blocking gate terminates evaluation and skips the rest.

Dispatch Pipeline (GOVERNANCE_GATE_SEQUENCE)

Used for step_dispatch and delegated_run_dispatch. This is the full pipeline that evaluates every dispatch request before an agent executes work.
// convex/trust_fabric/gates/index.ts
export const GOVERNANCE_GATE_SEQUENCE = [
  "gatewayHealth",       // 1. Is the gateway online?
  "agentStatus",         // 2. Is the agent available (not paused/terminated)?
  "concurrency",         // 3. Is the agent below its concurrency limit?
  "rateLimit",           // 4. Is the agent within rate limits?
  "budgetAgent",         // 5. Has the agent exhausted its monthly budget?
  "budgetEnvelopes",     // 6. Are all budget envelopes within limits?
  "trustLevel",          // 7. Does agent trust level meet gateway minimum?
  "contextTrust",        // 8. Does dispatch context meet role trust requirements?
  "policyRules",         // 9. Do dispatch policies allow this action?
  "approvalRequired",    // 10. Does a policy require approval for this action?
] as const;
The engine also evaluates two gates not listed in the sequence constant:
  • Gate 2b: identity — validates NHI (Non-Human Identity) credentials when present. Runs between agentStatus and concurrency.
  • Two-Agent Rule — injected into the approval gate for Rung 4 (bounded) agents when the action is classified as financial and estimated cost exceeds the $100 threshold.
Action-type-aware branching:
  • step_dispatch: concurrency gate runs normally.
  • delegated_run_dispatch: concurrency gate is skipped (delegated runs manage their own concurrency). Budget gate checks against a guardrails cost ceiling instead of just monthly spend.

Launch Pipeline (LAUNCH_GATE_SEQUENCE)

Used for workflow_run_launch preflight. A lighter sequence because there is no specific agent yet at launch time.
// convex/trust_fabric/gates/index.ts
export const LAUNCH_GATE_SEQUENCE = [
  "authorityPosture",   // 1. Does the selected posture allow run creation?
  "gatewayHealth",       // 2. Is the gateway online?
  "workflowReadiness",   // 3. Does the workflow exist with a valid version?
  "policyRules",         // 4. Do run_creation policies allow this?
  "approvalRequired",    // 5. Does a policy require approval?
] as const;

POC Reduced Enforcement

Agents in the poc lifecycle phase get reduced enforcement: gates 5—8 (budget, envelope budget, trust level, context trust) warn but do not block. The POC override downgrades a block to a pass with an audit detail noting the override:
// convex/trust_fabric/engine.ts
const POC_REDUCED_GATES = new Set([
  "budgetAgent", "budgetEnvelopes", "trustLevel", "contextTrust"
]);
The override is recorded in the GateOutcome.data with pocOverride: true for audit trail integrity.

The Hooks Pattern

Gates that need async data (DB queries, rate limiter mutations) cannot call the database directly — they are pure functions. The engine delegates these operations through the GovernanceEvaluationHooks interface:
// convex/trust_fabric/engine.ts
export type GovernanceEvaluationHooks = {
  /** Count running steps for concurrency gate. Only called for step_dispatch. */
  getRunningStepCount: () => Promise<number>;
  /** Check rate limit. Returns ok/retryAfterMs. */
  checkRateLimit: () => Promise<{ ok: boolean; retryAfterMs?: number }>;
  /** Evaluate dispatch policies. Returns block/approval requirements. */
  evaluateDispatchPolicies: () => Promise<PolicyEvaluation>;
  /** Evaluate approval policies. Returns matching requirements. */
  evaluateApprovalPolicies: () => Promise<ApprovalRequirement[]>;
  /** Check whether approval already exists for this step. */
  getApprovalStatus: () => Promise<{ hasApproved: boolean; hasPending: boolean }>;
};
The bindings layer creates these hooks by closing over the Convex ActionCtx:
// convex/trust_fabric/bindings.ts (inside runGovernanceGates handler)
const hooks: GovernanceEvaluationHooks = {
  getRunningStepCount: () =>
    ctx.runQuery(internal.stepResults.countRunningByAgent, {
      agent: args.agentId,
    }) as Promise<number>,

  checkRateLimit: () =>
    ctx.runMutation(internal.lib.rateLimits.checkAgentDispatchRate, {
      agentId: args.agentId,
    }) as Promise<{ ok: boolean; retryAfterMs?: number }>,

  evaluateDispatchPolicies: () =>
    evaluateDispatchPolicies(ctx, args.gatewayId as string, policyCtx),

  evaluateApprovalPolicies: () =>
    evaluateApprovalPolicies(ctx, args.gatewayId as string, policyCtx),

  getApprovalStatus: () =>
    ctx.runQuery(internal.trust_fabric.approvals.mutations.getStepApprovalStatus, {
      runId: args.runId as Id<"workflowRuns">,
      stepId: args.stepId,
      source: "policy",
    }) as Promise<{ hasApproved: boolean; hasPending: boolean }>,
};
Why this pattern matters:
  • The engine (engine.ts) is fully testable without a database — pass mock hooks.
  • Gates remain pure functions — the engine calls hooks at the right point in the sequence.
  • The binding layer is the only place that touches Convex ctx.
The launch pipeline has a simpler hooks interface (LaunchEvaluationHooks) with just policy evaluation, no concurrency or rate limiting.

Bindings

bindings.ts is the Convex action layer that wires the pure engine to the database. It exports two main internalAction handlers:

runGovernanceGates

The primary dispatch governance entry point. Steps:
  1. Fetch data — gateway doc, agent doc (via resolveForExecution), budget envelopes, registry entry
  2. Registry freshness check — blocks dispatch if agent has no registry entry (shadow IT prevention)
  3. Resolve governance context — calls resolveGovernanceContext() from role_resolver.ts to get effective role, posture, identity, bounds, and scope
  4. Build hooks — closes over ctx to create GovernanceEvaluationHooks
  5. Rate limit pre-check — token-based rate limit check before the full pipeline
  6. Evaluate — calls evaluateGovernance(evalContext, hooks) from the engine
  7. Audit — schedules governance audit log write and trace event (fire-and-forget, never blocks dispatch)

runEdgeGovernanceGates

Same as runGovernanceGates but for edge/Convex-native agents with no real gateway. Uses a synthetic healthy gateway doc. Skips gateway-scoped policies when no gatewayId is provided.

Legacy wrappers

runSafetyGates and runDelegatedSafetyGates provide backward compatibility — they call runGovernanceGates internally and convert the GovernanceDecision to the legacy SafetyGateResult format via toLegacySafetyGateResult().

Re-exports

bindings.ts re-exports all public types, gate functions, and engine entry points. Existing consumers that import from trust_fabric/bindings continue to work without import changes.

Role Resolver

role_resolver.ts contains pure functions that centralize precedence logic for role, identity, and authority resolution. The engine consumes its output (GovernanceResolutionContext) — it never fetches data itself.

resolveGovernanceContext (primary entry point)

Packages six resolution steps into a single result:
// convex/trust_fabric/role_resolver.ts
export function resolveGovernanceContext(params: {
  agent: MinimalAgentDoc;
  workerRole: MinimalWorkerRoleDoc | null | undefined;
  run?: MinimalRunDoc;
  sessionId?: string;
  dispatchType?: "step" | "delegated_run";
  parentActorId?: string;
  platformDefaultPosture?: ExecutionAuthorityPosture;
}): GovernanceResolutionContext
Returns:
  • effectivePosture — the authority posture in effect
  • postureSource — where it came from (run_override | role_default | platform_default)
  • roleSnapshot — resolved role with autonomy tier, authority bounds, trusted context config, scope
  • actorIdentity — structured ActorIdentityV1 envelope
  • resolvedBounds — merged authority bounds (role defaults + agent overrides)
  • resolvedTrustedContext — role’s trusted context config for the context trust gate
  • resolvedScopeConstraints — environment, tool, and data access constraints from the role scope

Posture Precedence

run-level override  >  role default  >  platform default ("bounded_autonomous")
// convex/trust_fabric/role_resolver.ts
export function resolveEffectiveAuthorityPosture(
  runPosture?: string,
  roleSnapshot?: RoleSnapshot,
  platformDefault?: ExecutionAuthorityPosture,
): PostureResolution
Valid postures: advisory, retrieval, approval_required, bounded_autonomous. Invalid values are silently skipped in the precedence chain.

Authority Bounds Merging

Role-level bounds serve as defaults. Agent-level fields (maxConcurrentSteps, budgetMonthlyCents) override them:
// convex/trust_fabric/role_resolver.ts
export function resolveEffectiveAuthorityBounds(
  roleSnapshot?: RoleSnapshot,
  agentOverrides?: {
    maxConcurrentSteps?: number;
    budgetMonthlyCents?: number;
  },
): Partial<AuthorityBounds>

Dispatch Context Provenance

buildDispatchContextProvenance() constructs a ContextProvenanceV1 describing the trustworthiness of the dispatch context. At dispatch time, the context is always internal_verified (Convex-managed). Freshness is derived from the run’s creation time against the role’s maxFreshnessMinutes (default: 30 minutes).

Explanation Builder

explanation.ts is a pure function that derives a structured GovernanceExplanation from a GovernanceDecision. Every decision gets an explanation attached automatically by buildGovernanceDecision(). The explanation answers three questions:
  1. What happened?outcome + summary
  2. Why?reasons (one per evaluated gate, skipped gates excluded)
  3. What could change it?unblockHints (only when concrete threshold data exists)

Explanation Reasons

Each gate outcome maps to an ExplanationReason with:
  • category — one of: authority, trust, scope, policy, budget, health, approval, concurrency, readiness
  • gate — which gate produced it
  • outcomepass, block, hold, or skip
  • summary — human-readable description
  • trustAspect — for trust failures: source_class, freshness, or environment
  • enforcedLimit — for budget/concurrency/trust failures: the threshold and current value
  • scopeAction — whether the system blocked execution or merely conveyed constraints

UnblockHints

Only emitted when the system has concrete data to make the hint honest. Each hint carries:
// convex/trust_fabric/types.ts
export type UnblockHint = {
  category: ExplanationCategory;
  gate: string;
  hint: string;
  confidence: "enforced" | "advisory";
  thresholdData?: {
    field: string;
    currentValue: number | string;
    requiredValue: number | string;
    delta?: number | string;
  };
};
  • enforced confidence means there is concrete threshold data backing the hint (e.g., “Agent budget exhausted: 5000/5000 cents”).
  • advisory confidence means the hint is best-effort guidance without exact numbers (e.g., “Gateway is offline. Wait for it to come back online.”).
When the system cannot suggest a concrete remediation, it returns an empty unblockHints array rather than inventing certainty.

Adding a New Gate

To add a new gate to the dispatch pipeline:

Step 1: Create the gate file

Create convex/trust_fabric/gates/gate_N_yourgate.ts following the naming convention:
// convex/trust_fabric/gates/gate_N_yourgate.ts
import type { GateContext, SafetyGateResult } from "./index";

/**
 * Gate N: Your gate description -- STD-XX.
 *
 * Pure function (no DB access, no side effects).
 */
export function yourGate(ctx: GateContext): SafetyGateResult {
  // Check your condition
  if (someFailureCondition(ctx)) {
    return {
      pass: false,
      errorCode: "your_error_code",
      retryable: false,  // or true if the caller should retry
      message: `Human-readable explanation of why this blocked`,
    };
  }
  return { pass: true };
}

Step 2: Export from the gate index

Add your gate to convex/trust_fabric/gates/index.ts:
export { yourGate } from "./gate_N_yourgate";
If this gate belongs in the standard sequence, add its name to GOVERNANCE_GATE_SEQUENCE:
export const GOVERNANCE_GATE_SEQUENCE = [
  "gatewayHealth",
  "agentStatus",
  "concurrency",
  "rateLimit",
  "budgetAgent",
  "budgetEnvelopes",
  "trustLevel",
  "contextTrust",
  "policyRules",
  "approvalRequired",
  "yourGate",            // <-- add here
] as const;

Step 3: Wire into the engine

In convex/trust_fabric/engine.ts, add the gate evaluation at the appropriate point in the evaluateGovernance() function:
// ── Gate N: Your gate ─────────────────────────────────────────────
const yourResult = yourGate(gateCtx);
const yourOutcome = gateOutcomeFromResult("yourGate", yourResult);
gateOutcomes.push(yourOutcome);
if (!yourResult.pass) {
  const gates = appendSkippedOutcomes(
    gateOutcomes,
    GOVERNANCE_GATE_SEQUENCE.slice(N),  // skip remaining gates
    "blocked_by_previous_gate",
  );
  return buildGovernanceDecision({
    ...enrichedBase,
    gates,
    blockedBy: {
      gate: "yourGate",
      errorCode: yourResult.errorCode,
      message: yourResult.message,
      retryable: yourResult.retryable,
    },
  });
}

Step 4: Add explanation support

In convex/trust_fabric/explanation.ts:
  1. Add a category mapping in GATE_CATEGORY_MAP:
const GATE_CATEGORY_MAP: Record<string, ExplanationCategory> = {
  // ... existing mappings
  yourGate: "your_category",  // pick from existing categories or add a new one
};
  1. Add a passed-gate summary in summaryForPassedGate():
case "yourGate":
  return "Your gate check passed";
  1. Add unblock hints in buildUnblockHints() if you have concrete threshold data:
case "yourGate":
  hints.push({
    category: "your_category",
    gate: "yourGate",
    hint: "Concrete guidance on what would need to change.",
    confidence: "enforced",
    thresholdData: { field: "...", currentValue: ..., requiredValue: ... },
  });
  break;

Step 5: If your gate needs async data

If your gate requires DB queries, do not add DB access to the gate function. Instead:
  1. Add a new hook to GovernanceEvaluationHooks:
export type GovernanceEvaluationHooks = {
  // ... existing hooks
  yourAsyncOperation: () => Promise<YourData>;
};
  1. Wire the hook in bindings.ts inside the hooks object:
const hooks: GovernanceEvaluationHooks = {
  // ... existing hooks
  yourAsyncOperation: () =>
    ctx.runQuery(internal.yourModule.yourQuery, { ... }),
};
  1. Call the hook in the engine before your gate:
const yourData = await hooks.yourAsyncOperation();
const yourResult = yourGate(gateCtx, yourData);

Step 6: Test

Write a unit test for your gate function. Since gates are pure functions, they can be tested without any Convex runtime:
import { yourGate } from "./gate_N_yourgate";

test("yourGate blocks when condition is met", () => {
  const result = yourGate({ /* minimal GateContext */ });
  expect(result.pass).toBe(false);
  expect(result.errorCode).toBe("your_error_code");
});

test("yourGate passes when condition is not met", () => {
  const result = yourGate({ /* minimal GateContext */ });
  expect(result.pass).toBe(true);
});

All Gate Files Reference

FileGateWhat it checks
gate_1_preflight.tspreflightGateOPA/Rego deploy-time policy (stub — enforcement in CI/CD)
gate_2_identity.tsidentityGateNHI credential expiry (graceful degradation when no NHI)
gate_3_safety.tsgatewayHealthGateGateway online/degraded/offline status
gate_3_safety.tsagentStatusGateAgent lifecycle status (paused/terminated/error)
gate_3_safety.tsconcurrencyGateRunning step count vs max concurrent limit
gate_3_safety.tsagentLifecycleGateAgent lifecycle stage (active/inactive)
gate_4_budget.tsbudgetGateAgent monthly budget exhaustion
gate_4_budget.tsenvelopeBudgetGateBudget envelope exhaustion (per-scope, per-period)
gate_5_context_trust.tscontextTrustGateContext source class, freshness, environment eligibility
gate_6_policy.ts(hooks)Dispatch policy evaluation (wired through engine hooks)
gate_7_scope.tsscopeGateCredential scope / network policy (stub — future Aembit integration)
gate_8_autonomy.tstrustLevelGateAgent trust level vs gateway minimum
gate_8_autonomy.tsautonomyGateAutonomy rung enforcement (assistive/retrieval/supervised/bounded)
gate_9_guardrails.tsguardrailsGateOutput guardrails (stub — Bifrost enforces externally)
two_agent_rule.tstwoAgentRuleCheckFinancial threshold for dual approval at Rung 4 ($100 / 10,000 cents)

GovernanceDecision Shape

The final output of every evaluation. Consumed by the interpreter, audit trail, and explanation builder:
// convex/trust_fabric/types.ts
export type GovernanceDecision = {
  disposition: "pass" | "block" | "hold";
  dispatchType: "step" | "delegated_run" | "launch";
  agentId: string;
  gatewayId: string;
  runId: string;
  stepId: string;
  gates: GateOutcome[];
  blockedBy?: { gate: string; errorCode: string; message: string; retryable: boolean };
  heldBy?: { policyName: string; policyId: string; trigger: string };
  budgetSnapshot?: { ... };
  trustSnapshot?: { agentLevel: number; gatewayMinimum?: number };
  contextTrustSnapshot?: ContextTrustSnapshot;
  scopeConstraints?: ScopeConstraintsV1;
  actorIdentity?: ActorIdentityV1;
  roleSnapshot?: { roleId: string; roleName: string; autonomyTier: string };
  postureSource?: "run_override" | "role_default" | "platform_default";
  actionType?: GovernanceActionType;
  explanation?: GovernanceExplanation;
  evaluatedAt: number;
  durationMs: number;
};
Three dispositions:
  • pass — all gates passed, execution may proceed.
  • block — a gate rejected the action. blockedBy identifies which gate and why.
  • hold — a policy requires approval before proceeding. heldBy identifies the policy.