Security

Written for your IT team to pick apart

You will not read here that security is our priority. You will read which mechanism protects which thing, with which number, and at the end a list of what is not built yet.

  • 23 tables with row level security in FORCE mode
  • 4 independent isolation layers between companies
  • 55 test suites that run on every change
  • 20 s maximum life for the cage the code runs in

Isolation between companies

Four layers that do not lean on each other

The question every head of IT asks is whether the neighbour can read their documents. The short answer is no. The long one is that you would have to break four separate things to manage it.

01

The query carries the company inside it

Every row belonging to a company carries its identifier, and the active company is taken from the session’s verified header, never from the request body. Both legs of the search, the semantic one and the exact-term one, carry the company condition inside the SQL. It is not a filter applied to the result.

02

The database checks it again

PostgreSQL enforces row level security in FORCE mode over the 33 tables that carry a company identifier. The identifier is set inside the transaction, and that scope is transactional, so a connection reused from the pool cannot drag the value into the next request. A test verifies that every new table with a company identifier has the policy switched on, and it breaks the integration run if anybody forgets.

03

The file system keeps the folders apart

One directory per company, created in mode 0700 and owned by the service user. Even if the application code got it wrong, the operating system will not let one company’s content be read from another. The directories are reconciled idempotently on every install.

04

The graph has its own namespace

One namespace per company identifier, keyed on the UUID rather than the short name, so renaming a company never orphans its graph. The client never sends graph queries: the service builds them and validates any path against that company’s own documents before touching anything.

A dedicated suite tries to cross those boundaries on every change, with concurrent requests interleaved between companies. It is the test we most want to keep failing the day somebody breaks something.

Authentication and exposed surface

The boring parts, done

None of these lines is novel. Every one of them is the kind you notice missing once something has already happened.

Identity and session

  1. Passwords are stored under argon2.
  2. A fifteen-minute access token and a seven-day refresh token, signed with separate secrets.
  3. The refresh token carries a version number: logging out invalidates every live session on that account immediately.
  4. A per-IP brute force limit on login and on refresh.
  5. Public signup is switched off. Accounts are created by a person.
  6. An administrator can only touch roles strictly below their own, and the last active owner cannot be demoted or deleted.

HTTP surface

  1. helmet and hpp at boot, not as a future plan.
  2. CORS with an explicit allowed-origin list, no wildcard.
  3. Request body capped at 100 kb.
  4. Global validation that rejects unknown fields instead of quietly ignoring them.
  5. A hundred requests a minute as the general limit, plus per-company limits on the agent and search routes.
  6. Errors carry a correlation id and never return the exception trace.

Three separate front doors

  1. The product application, the company administration console and the platform console are three distinct entrances.
  2. The platform console also requires the platform administrator flag: a perfectly valid company account is refused there and pointed at its own entrance.
  3. A platform administrator reaches a company’s data only by entering that company explicitly.
  4. One person can belong to several companies with a single email, and switching between them is one click that rebuilds the whole session scope.

Company secrets

  1. Stored encrypted with pgcrypto and decrypted only inside the process.
  2. No endpoint returns the value. The list is of names, and the interface shows Value hidden.
  3. The tab belongs to the company owner alone.
  4. They are injected into a sandboxed run only when the tool asks for them explicitly. By default none is injected.
  5. An ingestion filter blocks .env, .pem and .key files, credentials and SSH identities before indexing, so a secret reaches neither the vault nor the index.

The agent

How far it reaches and where it runs

An agent that writes files is useful precisely because of what it can do. What your IT team needs to be able to point at is where it stops.

Its boundary is its department mount

Every agent chat works inside the file subtree of its own project. Parent projects and connected projects are not mounted: the agent knows them through the summary we inject, carrying the description, the memory, the open tasks and a file listing. We put it that way, rather than as a per-file permission, because that is what the system enforces today.

A real failure we fixed

A session with no path assigned fell through to the company root directory, and from there one department’s chat could read another’s files. Today a path that cannot be resolved returns a directory isolated per company, never the vault root. We tell you because it is exactly the kind of bug a vendor would rather not mention.

Every tool lands in a hash chain

Every time the agent dispatches a tool, a row is written with the hash of the arguments, whether it finished cleanly, how long it took, and the previous row’s hash chained to this one’s. Editing or deleting a row breaks the chain and it shows. The raw arguments are never stored, only their hash. On top of that, every AI call leaves a line in a monthly log on disk recording the size of the text covered, never the content.

The cage the code runs in

When the agent runs Python, it runs in a rootless container that is born for that execution and dies when it ends.

The permission is checked before the container is created, so a request from somebody without the role never gets anything started.

Network
None. The container starts with no network interface.
Root file system
Read only.
Scratch space
64 MB, no execution and no setuid.
Kernel capabilities
All dropped.
Privilege escalation
Blocked.
User
Unprivileged, outside every system group.
Memory
512 MB, no swap.
CPU
One.
Processes
128 at most.
Maximum lifetime
20 seconds of wall clock, then it is killed.
Mounts
One, read only.

Operations

What happens when something goes wrong

The part nobody looks at in the demo and everybody looks at on the first bad day.

If you would rather none of this ran on our infrastructure, the Enterprise tier installs the whole platform on your server, with the same backups and the same proxy. See plans

Daily backup, fourteen-day rotation

At 03:30 both databases are dumped with pg_dump along with the global roles. They are kept for fourteen days.

Vault snapshots by hardlink

Files that did not change are linked to the previous day’s snapshot, so each day on disk costs only what moved.

Nothing listens outward

Every service binds to 127.0.0.1. The only reachable thing is the reverse proxy, and its configuration travels inside the repository.

One command reinstalls the platform

Database, embedding server, cache, graph and backup timers all come up with a single idempotent command. Running it twice breaks nothing.

Deleting a company deletes all of it

Usage logs, permissions, sessions, messages, notifications, secrets and threads go with it. Tests verify that nothing is left orphaned.

A watchdog every 45 seconds

A health probe restarts only the service that stopped answering, which is the case a process supervisor misses because the parent process is still alive.

No decoration

What we do not have

Your IT team is going to ask for this list, and if we do not publish it they will find it anyway. It is cheaper for both of us that it is here.

  1. We hold no certifications

    No SOC 2, no ISO 27001, and no security audit performed by a third party. The intrusion testing we run is our own and it does not substitute for an external auditor. If your procurement process requires a report signed by somebody outside, we do not have one today.

  2. The agent’s isolation is a container, not a microVM

    Rootless, no network, read-only root and no capabilities, which is a serious boundary. But it shares the host kernel, and anyone evaluating this knows the difference against a lightweight virtual machine. There is a written design for getting there. It is not built, and until it is it does not appear on this page as though it were.

  3. The agent reaches the internet with no destination allowlist

    Python execution runs with no network, and that is literal. But the box the agent itself runs in does have egress and we do not yet filter where to. They are two different things and blurring them would be selling smoke.

  4. Documents are not encrypted by the application at rest

    There is TLS in transit, secrets encrypted with pgcrypto in the database, and whatever encryption the server disk provides. Saying everything is encrypted at rest would be false: the vault is Markdown files readable by anyone with access to the server’s file system, and that access is governed by mode 0700 and by whoever administers the machine.

  5. The agent’s writes are bounded by mount, not by file

    The permission-by-permission check exists in the tool surface and is tested, including the case of a reader denied a write. It is not on the active path by default, so today’s real boundary is the department folder mounted into the container.

  6. We do not guarantee data residency

    It depends on where the installation is hosted and on the model provider. If your company needs data not to leave a jurisdiction, the only honest route is self-hosting, and we will gladly confirm in writing what the managed installation meets before you sign.

  7. The automatic budget cutoff is not wired up

    The precheck is written and tested, but no agent route calls it yet. You see exactly what you spent and we tell you, and that is what we can promise today.

If anything on this list is a hard requirement at your company, say so in the first conversation. We would rather lose the sale than have you find out during an audit. ventas@ofivia.com

The usual ones

Security questions

The five that come up in every review, with the short answer.

All the questions
Can another company see my data?
No, and it is blocked in four independent layers. Every row carries your company identifier. PostgreSQL enforces row level security in FORCE mode across 23 tables, keyed on the identifier taken from the verified session and never from the request body. Your company's folder is mode 0700. And the graph uses one namespace per company. A dedicated test suite tries to cross those boundaries on every change.
Do you have SOC 2 or ISO 27001?
No, and we would rather say so. No SOC 2, no ISO 27001, no external third-party audit. What we do have can be inspected: four-layer per-company isolation, argon2 password hashing, instant session revocation on logout, encrypted secrets that no endpoint ever returns, code execution in containers with no network and all capabilities dropped, and 55 test suites that run on every change, one of them dedicated to breaking cross-company isolation.
How do you protect passwords and sessions?
Passwords are hashed with argon2. A session uses a short access token of fifteen minutes and a refresh token of seven days, signed with distinct secrets. The refresh token carries a version number, so logging out invalidates every live session on that account instantly. Login and refresh are throttled per IP against brute force, and public self-registration is disabled.
Where does the code the agent runs actually execute?
In a rootless container created for that run and destroyed when it ends. No network, read-only root filesystem, all capabilities dropped, no privilege escalation, 512 MB of memory, one CPU, a process cap, an unprivileged user and a twenty-second wall-clock kill. A viewer or a member without permission never even starts the container: the request is refused first.
Can I review what the agent did?
Yes. Every tool the agent dispatches leaves a row with the hash of its arguments, whether it finished cleanly, how long it took, and the previous row's hash chained into this one. Editing or deleting a row breaks the chain and shows. Raw arguments are never stored, only their hash. Separately, every AI call appends a line to a monthly log file on disk that you can download.

Bring the person who is going to say no

If your head of IT has an objection, better to hear it before the migration than after. We will sit down with them.

  • No certifications we do not hold
  • Self-hosting available
  • Your files leave with you