Fibric. Docs fibric.io →
v1.0.0 ยท stable
Platform

Security model

Fibric runs AI operators against systems that matter, which is exactly why the platform is built so that the AI cannot be trusted and does not need to be. Every side effect passes a deterministic, default-closed trust gate; credentials never enter a prompt; and the database enforces the tenant wall on every row. This page describes the security model end to end: what is enforced, where it is enforced, and what an operator can never do.

Our posture

Fibric is in a v1.0 preview program. We do not hold third-party certifications today, and we do not claim them. What we offer instead is a security model you can read: the trust gate, the executor, and the row-level isolation policy documented on this page are the mechanisms that run in production, and the kernel source they are drawn from uses the same names this page uses. Where a control is architectural, we describe the architecture. Where a control is operational, we describe the operation. Where something is not yet built, we say so.

The design principle underneath everything is fail closed. A missing policy blocks the action. A missing tenant context returns zero rows. An unrecognized capability binds to nothing. The safe state is the default state, and reaching an unsafe state requires an explicit, recorded grant.

The fail-closed trust gate

No operator in Fibric makes a raw side-effecting call. An operator proposes an ExecutionPlan; the deterministic executor evaluates every side-effecting action in that plan against the tenant's trust policies before anything runs. The evaluation function is small enough to audit in one sitting, and its default is the point:

packages/kernel/src/trust.ts
// Default-CLOSED: a side-effecting action must be explicitly allowed by a matching
// policy whose constraints all pass. No matching policy → BLOCK.
export function evaluate(
  policies: TrustPolicy[],
  action: PlannedAction,
  env: EventEnvelope,
): TrustDecision {
  const matches = policies.filter(
    (p) =>
      (p.connector === undefined || p.connector === action.connector) &&
      (p.tool === undefined || p.tool === action.tool),
  );
  if (matches.length === 0) return 'BLOCK'; // fail closed
  for (const p of matches) {
    if (p.maxValue !== undefined && (action.value ?? 0) > p.maxValue) return 'BLOCK';
    if (p.predicate && !p.predicate(action, env)) return 'BLOCK';
  }
  return matches.some((p) => p.decision === 'ALERT') ? 'ALERT' : 'ALLOW';
}

The three decisions have exact meanings, described in depth in trust tiers:

DecisionMeaningWhat happens
ALLOW The action matched at least one policy and passed every constraint on every matching policy. The executor runs it, once, and writes a receipt.
ALERT The action is permitted but a matching policy demands a human in the loop. The plan pauses for approval through the Actions & plans API; the approval or veto is receipted with the approver's identity.
BLOCK No policy matched, a maxValue ceiling was exceeded, or a predicate returned false. The action never reaches the connector. The refusal is receipted. Approval cannot override a BLOCK; only changing the policy can.
i
The model is untrusted by design

Nothing in this gate consults the model's confidence, its reasoning, or its intent. A prompt injection that convinces an operator to propose a hostile action produces a proposal that hits the same wall every other proposal hits. The security boundary is the deterministic gate, not the model's alignment.

Capability grants

Operators do not talk to connectors by name; they request capabilities such as orders.hold, and the tenant's bindings decide which installed connector fulfills each one. This indirection is a security control, not only a convenience:

Policies compose with grants. A typical tenant configuration allows reads broadly, allows low-value writes on specific tools, and routes higher-value writes through ALERT:

guardrails for one operator
{
  "policies": [
    { "connector": "orders", "tool": "hold",   "decision": "ALLOW" },
    { "connector": "orders", "tool": "refund", "maxValue": 100, "decision": "ALERT" }
  ]
}

Under these two policies, a proposed orders.cancel is blocked because nothing matches it; a $250 refund is blocked because it exceeds maxValue; a $40 refund pauses for human approval. The operator's prompt, tone, and reasoning change none of this.

What operators can never do

These are structural properties of the kernel, not configuration you must remember to apply.

An operator can neverBecause
Call a source system directly Operators emit plans; only the deterministic executor invokes connector tools. There is no network path from the reasoning step to a source system.
See or emit a credential Credentials are injected into the connector runtime's HTTP client, never into the operator's context. See Secrets and credentials.
Read another tenant's data Every read runs inside a tenant-scoped transaction under Postgres row-level security, enforced for the table owner too. See Tenancy & isolation.
Take an ungoverned side effect Actions on tools marked sideEffecting pass the trust gate; the default with no matching policy is BLOCK.
Exceed a value ceiling maxValue is compared by the executor against the action's declared value; approval cannot raise it.
Repeat a side effect Every side-effecting action carries an idempotency_key; a replay disposes as DEDUP and does not run. See Single-flight & idempotency.
Act without leaving a record Every disposition, including refusals, is written to the tenant's receipt ledger.
!
The incident that shaped this design

The single-flight and idempotency primitives exist because of a real failure: an early agent, running without them, sent 657 messages into one customer conversation. Under the current kernel that flood is structurally impossible; the second send with the same idempotency key deduplicates, and concurrent work on the same conversation serializes on its entity_key.

Tenant isolation

Isolation between tenants is enforced in the database, not in application code. Every tenant-scoped row carries reseller_id and tenant_id; Postgres row-level security is enabled and forced on every tenant table, so the policy binds the table owner as well; and the application role, fibric_app, holds no BYPASSRLS attribute. The tenant context is set per transaction from a verified identity, never from a client-supplied header, and a request without a context reads nothing. The full mechanism, including the verbatim policy and the visibility checks that prove it, is documented in Tenancy & isolation.

Three consequences matter for security review:

Secrets handling

Connector credentials are stored in a managed secret store, encrypted at rest, and keyed per tenant. The connector runtime resolves them into a per-tenant HTTP client at call time; the operator, the model, the plan, the receipt, and the platform logs never contain them. API keys authenticate requests as one tenant and are shown once at creation. The complete treatment, including auth kinds, rotation, and redaction, is in Secrets and credentials.

Platform access

Requests authenticate with a per-tenant secret API key as a Bearer token (see the API overview). The key, not any value in the request body, determines the tenant a request acts as; a caller cannot name its own tenant. Keys carry fixed scopes, are revocable individually, and revoked keys fail with key_revoked (Errors). Transport is TLS; plaintext HTTP is not served. Console sign-in for Enterprise tenants can federate to your identity provider through SSO/SAML, which is in preview.

Responsible disclosure

If you believe you have found a vulnerability in Fibric, the platform, the SDKs, the CLI, or this site, report it to security@fibric.io. Include the steps to reproduce, the tenant or workspace involved if any, and what you observed. We ask that you do not access data that is not yours, do not degrade the service for others, and give us a reasonable window to remediate before public disclosure. We will acknowledge your report, keep you informed of progress, and credit you if you wish.

i
Keep reading

The security model is distributed across the concepts it protects: Governance & trust for the proposal-and-disposal loop, Trust tiers for policy anatomy, Single-flight & idempotency for the concurrency primitives, and Receipts & audit for the evidence trail.