Safe by design

A language model is good at reading a question and bad at being a guarantee. Fidara uses the model to understand and uses code for everything that must be exactly right. Each entry gives the decision, the reason, what code does in place of the model, and what it rules out.

In short

  • The model reads the question and writes a plan; code checks it, applies scope and builds the SQL.
  • Your app keeps authentication, credentials and row-scope rules; the model never types a number into a file.
  • When a name or row is unclear, Fidara asks; writes are held for approval and read back.

The model plans, code executes

The model writes an IR, not SQL

Decision
The model writes the question as a structured JSON query plan, the intermediate representation (IR). It never sees or writes SQL. Fidara validates the plan, critiques it and emits parameterized SQL with code.
Why
If the model writes the SQL, row isolation is whatever it emitted that time, and "usually correct" is not a security property. SQL is also not one language: engines disagree, often by returning a wrong number instead of an error. With an IR, each rule is fixed once per engine. Validation runs before anything executes, and the plan is a readable audit trail.
Code does
Checks that every table and column exists, that joins connect and match the foreign keys the catalogue declares, that output names are unique, and that a join does not multiply a sum (the fan-out check). Then the emitter builds a parameterized SELECT. Values are bound parameters.
Rules out
Model-written SQL reaching your app. Prompts that teach the model each engine's quirks. Writing from the query path: INSERT, UPDATE, DELETE and DROP cannot come out of the emitter, whatever the model produces, and SQL injection through the question is structurally ruled out.
LimitsOne exception, inside Deep Research only: to combine results it has already fetched, the research model may write a read-only DuckDB SELECT over those rows. The rows were fetched through the path above, in the user's scope, and that query never reaches your app's database.

One rule per database engine, written once

Decision
Each engine's rules live in an engine profile, written once and tested on the real engine.
Why
Many engine differences give a different number with no error. Fixing each once means every question inherits the fix.
Code does
Applies the profile in the emitter. For example, a bare standard deviation is the population formula on MariaDB and the sample formula on PostgreSQL, and the emitter always asks for the sample formula.
Rules out
A model that has to know the exact engine and version on every question and never slip.

The plan cache replays code, not model output

Decision
On a cache hit the model is skipped. The stored IR goes straight to the emitter.
Why
The same question then costs no model call, and cannot leak one user's scope to another.
Code does
Stores only plans that were built without retries. The scope is never stored with a plan and is injected again on every run. A plan is never replayed against a different catalogue or semantics overlay: a fingerprint of both must match.
Rules out
Replaying a stored scope. Replaying a plan against a changed catalogue.

The local engine: the same exact calculation, wherever the data lives

Decision
The model plays no part in how a calculation runs. Each plan is split in two stages.
Why
Scope is applied only in stage one, so stage two can never see more than the user could fetch. A result over the row cap is never presented as complete: the answer says the question needs too many rows and offers to narrow it.
Code does
Stage one runs on your app's database: the scoped fetch, always, and every aggregate that database can do. Stage two runs in DuckDB inside the Fidara process, over the rows stage one returned: percentiles, correlation, window functions and similar, where the database cannot. Money travels as exact decimals. DuckDB runs with file, network and extension access turned off.
Rules out
A calculation that runs on more rows than the user may fetch. An incomplete result presented as complete.
LimitsThe local engine and its differential tests (SQLite, PostgreSQL, MariaDB) are built. A general guarantee that every supported database gives the same answer is still being proven; today the databases agree where the engine profiles are tested. The shared library of business functions such as growth rates and fiscal periods is not built. The row-cap answer is unit-tested but has not been checked live.

Security is enforced, not taught

Authentication is the app's

Decision
Fidara takes a bearer token from the iframe address and calls the app's verify webhook on every request. It gets back a user id.
Why
The app already knows who its users are and when a session ends. Fidara never receives database credentials either: the app runs every query itself.
Rules out
A second login, a user store or token logic inside Fidara.

Row scope, denied columns and table visibility

Decision
The model is never asked to apply a user's scope and cannot change it. It is not shown the scope values.
Why
Row isolation must not be whatever the model happened to emit that time.
Code does
Your adapter returns, per user, a row_filter (SQL) or a record_scope (the same rule as data), a list of denied_columns, and only the tables that user may see. Fidara injects the scope into every statement after validation, including the bodies of CTEs and subqueries. A query that names a denied column is refused before any SQL is emitted, and the value is also absent from results. A table the scope does not name is refused (fail closed).
Rules out
Running with a guessed scope: a malformed record_scope, or a table that declares both forms, makes Fidara refuse the whole catalogue response.

Role-scoped catalogues, not one shared schema

Decision
The catalogue is fetched for each user. A sales rep, an accountant and a manager each see the tables, columns and rows their role may read.
Why
The same question has a different correct answer for each role. Fidara also builds nothing that would summarize rows some users may not read: its name matcher, for example, reads candidates through the requester's scope instead of keeping a shared value index.
Rules out
A single all-seeing schema view. A cache that mixes users' data.

Security is enforced; semantics are taught

Decision
The test is whether the value differs between two requesters. company = 'Acme' differs, so it is security: a structural filter in the row scope that the model cannot bypass. docstatus = 1 or disabled = 0 is the same for everyone, so it is a business default that the model can see and, when a question asks for it, lift. Business defaults are not security.
Why
A default forced into the row scope cannot be lifted and is invisible to the model, so questions about the excluded rows get confidently wrong answers, and nothing can detect it. Kept apart, the default stays visible and overridable. A security rule left to the model is not a guarantee.
Code does
A business default is declared as a semantic filter. It is applied automatically, and a question about the excluded rows can lift it by filtering that column. A default that records one fact in two columns lists the other column in same_fact_as, so lifting one lifts both. A semantics overlay file may carry labels, synonyms, defaults and metrics, but never security: the loader rejects any security key and the process refuses to start.
Rules out
Putting both kinds in one filter string. A semantics overlay that carries security.

Shared answers use the viewer's scope

Decision
A colleague link stores the question, not the sharer's data. The model plays no part.
Why
Sharing never widens access.
Code does
Opening the link re-runs the question with the viewer's own catalogue. A viewer who cannot see a table gets "not available".
Rules out
A shared answer that carries the sharer's rows.

Refusals and "not available" use fixed wording

Decision
The model does not write a refusal. Triage can decide to refuse, but the reply is a fixed template built from codes.
Why
A refusal cannot then leak a name or a value.
Code does
When a question needs data outside the user's access, the reply is the neutral sentence "isn't available with your access". An adapter may add a plain label for what is restricted, never a column name or a scope value. Whether a turn ended as "unavailable" or "unsure" is decided by code.
Rules out
Model-written refusal text.
LimitsFidara trusts the catalogue and scope your adapter returns. It never widens them, and it cannot know whether an adapter returned too much. A column that is secret only in combination, such as a margin that can be rebuilt from price and cost, needs all of its parts denied.

Writes and files

Writes are declared, dry-run, held and read back

Decision
Nothing is written without approval. The model proposes a change by naming an action the app declared in its catalogue. It never saves.
Why
The app performs the write through its own API as the requesting user, so the app's own rules apply.
Code does
Before approval: validates the proposal. For an import, sends every ready row to the app as a dry run: validate, save nothing. The app's own message is shown on the row and the cell, and Fidara never paraphrases it. The app also says whether it ran all of its rules or only the ones it could reach without saving.
On approval: the server holds the validated proposal. Apply carries only an opaque approval id that is one-shot, expiring and tied to the user. The declaration is read again first, and a withdrawn action is refused before anything is sent.
After: every response must echo the user as acted_as; a mismatch stops the run. Each result is read back from the app and marked read_back, app or disputed. A timeout is settled by reading the record, not by retrying. A row already written, or in doubt, is never sent again, so a second Apply cannot duplicate it.
Rules out
An invented action, or a field outside the action's allow-list. A client that can change values between approval and write. Writing as a service account.
LimitsReal writes are off by default in the Dolibarr, ERPNext and Odoo adapters and must be switched on for a run. A new record cannot be read back by key, so a create is confirmed by the app's answer. There is no automatic undo; the app owns the record, so a revert is manual.

One model call per import, and review approves the plan, not each row

Decision
The model writes one import plan: which source column goes to which field, type conversions and constants, validated against the fields of the action the app declared. Fidara then checks every row with the app's dry run, shows a summary such as "183 rows ready, 12 names unmatched", lets the user fix systematic problems once, and asks for one approval of the ready rows.
Why
Approving a plan after reading the summary is a stronger review than clicking 200 rows, because nobody reads row 147. One wrong mapping must not reach hundreds of real records before anyone looks. An import of 500 rows costs one model call and is auditable, and the import and a single-record edit use the same write grammar and the same validator.
Code does
Applies the plan to all rows. Names are looked up in live records by the matcher: exact, probable, several or none. Unresolved names stay as gaps.
Rules out
A "confirm all" button. Writing before the summary. A model call per row.

The model never types a number into a file

Decision
Every value written into a file comes from a query result in the requester's scope, or from a deterministic file operation run by code. Each changed cell keeps its source: the query, the as-of time, the record and the period.
Why
It is the file-side twin of "the model never writes SQL". A filled spreadsheet must be verifiable.
Code does
One model call produces a fill plan: which column to fill, which columns identify the record, and the query that gives the value. The plan can carry no literal result, and any number in the model's message is never read. Code validates the plan against the catalogue the user sees, builds the grouping, adds the key and period filters, and runs one query for all rows, not one per row. Rows are matched to records by the same matcher as imports. Periods are read by code, and an ambiguous date such as 3/4/2025 is refused. Only empty cells are written. A result that may have been cut is refused, not filled. Rows with no match, or an unclear match, are marked and sent to the user.
Rules out
Numbers copied from model text into a cell. Guessed matches.

Formulas stay formulas

Decision
The model turns "add a total row" or "margin = revenue minus cost" into a short list of operations from a fixed set. It writes no value.
Why
The downloaded file keeps working when the user edits it.
Code does
Parses a computed column into a syntax tree and writes it as a real spreadsheet formula, for example =C2-D2. Subtotals and totals use SUBTOTAL. Each formula also carries a cached value, so a previewer shows numbers without recalculating. Values from the app are plain numbers, each with its source and as-of time, listed on a Sources sheet. Every change is a new version, and any version can be downloaded.
Rules out
Pasting computed values where a formula belongs.

Files are kept on the server, per user

Decision
A file opened beside the chat is stored on the server. A reopened chat shows its files again. A changed file is saved as a new version and the original is kept. A file is private to its owner. Deleting a chat deletes its files, and each user has a storage quota.
Why
Work on a file spans sessions and devices. Versions allow undo and keep the user's original.
Rules out
Browser-only files that vanish with the tab. Sharing a file through a shared chat: a shared chat carries the question only.

Answers you can check

Name matching uses no model

Decision
The model never sees the stored names, so it can only copy what the user typed. A matcher in code maps the typed name to a stored record.
Why
A misspelt name would return zero rows, and zero rows looks like a real answer. The rules are set to avoid a confident answer about the wrong record, even at the cost of asking more often.
Code does
One scoped read fetches candidate values. Python ranks them by letters, by sound and by initials. The result is one of four: exact, one clear match (the answer says which record it used), several (the user is asked to pick), or none (the answer says no record matches). Sound alone never picks a record, and initials never pick one. A suggestion can only name a record the user could already read.
Rules out
Silent best guesses on a name.
LimitsA typed word that no stored value has can block a match. "Pioneer Machine Shop" will not match "Pioneer Machine, Inc." on its own.

Ask rather than guess

Decision
When a name could mean several records, or a file row matches nothing clearly, Fidara asks. Every agent that asks uses one contract: the agent says that it must ask and what, and the main loop shows the question, saves it and sends the answer back to the step that asked.
Why
A confident answer about the wrong record is worse than a question.
Rules out
Silent best guesses. Re-planning a multiple-choice answer as free text.

Deep Research figures are traced to queries

Decision
The model writes the sections of a report, but it is not trusted to cite correctly or to hide internal names. Code supplies the identifiers and the citations.
Why
A report carries no internal id or token, and its citations are numbered from sources that were actually traced to a query.
Code does
Gives every fetched result a plain label and a citation key before the writer sees it, so the writer has nothing internal to cite. After the writer, code checks the text again. Keys become numbered citations in report order, and unknown keys are removed. Leftover internal tokens are removed, and a bare "customer 387" becomes the record's display name. A denied column is stripped before the research agent sees any data. Analysis code that the agent writes runs in a container with no network and a read-only filesystem.
Rules out
Citing what was not queried. Internal ids in a report.

How Fidara fits your stack

Target systems are black boxes

Decision
An adapter reaches its app only through the app's own API or database views.
Why
The app's permissions, validation and upgrades stay the app's. Writes go through the app's own API as the requesting user, so the app's own rules apply.
Rules out
Changing an app's source, models or migrations. Writing as a service account.

Three webhooks, and the app keeps what is its own

Decision
The app keeps its database connection, credentials, login, row-scope rules, business vocabulary and list of sensitive columns, and exposes only verify, catalogue and query, plus an optional write. The full contract is on Integrate your app.
Why
An integration should not need Python packages or React components in the app.
Rules out
Handing Fidara database access. Installing Fidara code inside the app.

Reuse tested open tools with compatible licences

Decision
Document output and file reading use existing libraries: XlsxWriter for Excel files, python-docx for Word, WeasyPrint for PDF (an optional extra), SheetJS Community Edition for reading spreadsheets in the browser, DuckDB for the local engine. All are listed in the repository's licensing file.
Why
A maintained library with a compatible licence beats custom code. Where none fits, code is written: the spreadsheet formula grammar is about 250 lines because no permissive library both parsed column references and emitted the formula from the same tree.
Rules out
Dependencies whose licence conflicts with AGPL or with the permissive adapters. Hand-written code where a permissive, tested library does the job. The licences are on Integrate your app.

Three narrow extension hooks, and no more

Decision
Fidara has exactly three extension points: a settings file (environment beats file beats .env), a read-only semantics overlay for labels, synonyms, defaults and metrics, and entry-point plug-ins for extra agents and tools.
Why
Each is useful to anyone who simply self-hosts. Anything built on Fidara runs the released package unchanged.
Rules out
A general plug-in framework. Any hook that carries security. Plug-ins are Python in your process and read data only through the core's scoped path, which is a convention and not a sandbox.