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:
// 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:
| Decision | Meaning | What 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. |
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:
- The grant surface is enumerable. An operator's reachable side effects are exactly the side-effecting tools of the connectors bound to its declared capabilities, filtered by trust policy. There is no ambient authority to escalate into.
- Unbound means unusable. A capability no installed connector fulfills resolves to nothing; a plan that needs it fails with
capability_unbound(see Errors) rather than falling back to a broader tool. - Tools declare their nature. A connector tool is only treated as safe-to-read when its definition omits
sideEffecting: true. Anything side-effecting routes through the executor and the trust gate; a connector author cannot opt a write out of governance. - Revocation is immediate. Uninstalling a connector removes the binding; the platform refuses the uninstall with
connector_in_useif an active operator still depends on the capability, so a grant cannot be silently orphaned mid-flight.
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:
{
"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 never | Because |
|---|---|
| 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 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:
- A query bug or injection in one tenant's request surfaces zero foreign rows, because the policy is evaluated on every row the database would return.
- An operator senses and acts within one tenant transaction; there is no capability it can request that crosses the wall.
- The audit trail itself is tenant-scoped: your receipts live behind the same wall they document.
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.
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.