Kortix's open-source AI agent audit trail starts with action names
Kortix is open source, and its AI agent audit trail records every action under a stable name; filter, export and stream it on Enterprise.
An enterprise reviewer will ask what your agents did, and Kortix, the open-source CompanyOS, answers with an AI agent audit trail that names every action and the person the agent acted for. A chat transcript cannot; it records what the model said, not what the system allowed.
A thread on r/SaaS carries the buyer's version: an enterprise customer asks how to know what the AI did versus what a human did.
The log the agent writes about itself
Kubernetes records API server requests, the container runtime records stdout, the cloud provider records control-plane calls. In ARMO's post on the minimum viable audit trail (June 2026), Yossi Ben Naim points out that none of those three classes is written where the agent decides. A compromised agent has no incentive to print its actions, and one that narrates its reasoning can be told to stop.
The audit problem with AI agents is that the actor is also the narrator: its record is testimony, not evidence.
ARMO names the missing class the agent-action log, with a field list for six event types: a good specification, and one you deploy and maintain.
Kortix writes the row on the other side of the call. A request's row carries the action of its route, so the action is what the API did, not the model's account of it. Runtime events arrive separately, under opencode., written by the runtime that ran the tool.
What an audit trail has to record
An AI audit trail is the record of what a system did, kept in enough detail to reconstruct a decision months after the session ended.
Collibra calls it a chronological, traceable log of every consequential event, detailed enough to answer four questions: what data went in, what came out, what the system did with it, and whether it was allowed.
Kortix's own minimum is shorter. The security page answers with: "Who did what, when, how they signed in, and whether it was allowed, refusals included." A refused call is recorded, because a trail of successes cannot show the attempt that was stopped. And when an agent acts, the row names the person it acted for.
Both answers agree on the fields, and so does most guidance. What separates a trail that survives review is the vocabulary behind the actions.
The vocabulary is the trail
An audit row is only as useful as the string in its action column. Kortix names every action domain.resource.verb: gateway.key.revoke, agent.scope.update, connector.approval.denied. The web app and the CLI show a human title beside it; queries key on the action.
An export rule, an alert rule and a webhook filter all match on a string that must stay identical across releases, projects and years. A free-form sentence breaks all three: Revoked LLM gateway key reads well today, gets reworded later, and the rule that matched it stops matching without telling anyone.
The published list is the size of a real vocabulary: 800 request actions, 83 events outside a request, and 55 operator actions, checked October 2026. It is closed and public, so a reviewer can confirm an action exists before they need it.
gateway.key.revoke answers a security question about a credential. agent.scope.update answers a governance question about someone widening an agent's reach. connector.approval.denied answers the reviewer's question about a person saying no, and it stands as the request's own row, not the call it blocked.
A family suffix covers a surface without naming every call in it: connector.github.create_issue sits under the connector family that way.
What a row refuses to store
What a trail declines to keep is what lets the log be exported at all.
Every request row holds metadata.http, the route template, for example DELETE /v1/projects/:projectId/gateway/keys/:keyId. It does not hold the raw path. A path segment can be a link token, such as a setup or approval link, so a log of literal URLs would collect credentials in plain text.
A request that matched no route records api.route.unmatched, which shows a reviewer probing that never resolved to a handler. When a handler records a real outcome, that event replaces the request row. secret.strategy.changed appears only when the strategy actually changed. An approval records one row per outcome, connector.approval.approved or connector.approval.denied. A recorded event is the request's only row, so counting attempts stays honest.
| What a row keeps | What it refuses |
|---|---|
| One named action from the published set | A free-text description of the call |
| The route template of the request | The raw path and any token inside it |
| The person the agent acted for | An unnamed service identity |
| The refusal, with its reason code | Successes only |
api.route.unmatched for a dead end | Silence where nothing matched |
From a row to an answer
Filtering by prefix turns a log into a query: one prefix returns a whole surface without a join.
kortix audit ls --action gateway.
kortix audit webhooks add --action-prefix gateway.
The first line lists every gateway row: keys created, revoked, logs opened. The second streams that prefix to a webhook, so an alert rule outlives the release that adds the next gateway endpoint. The same prefix works as ?action=gateway. on the audit API, and on export and project.
Every action is recorded. Reading, exporting and streaming the audit log depend on the plan. The security page lists all three as an Enterprise feature. Recording is never gated, refusals included, so a smaller plan still has its rows when it upgrades. Events stay readable for 90 days and sit in a locked archive to day 365.
The audit view in the product is per session. The export and the webhook prefix reach past one session, so the naming convention sets how far your evidence reaches.
The approval that makes the row mean something
An approval is bound to one call: this connector, this action, these arguments, this account, used once. A yes for one refund does not carry to the next, and changing any part of the call voids it. A held call expires after 24 hours unless the project sets another time, anywhere from 5 minutes to 30 days. Only a project manager, or the person who started the session, can approve it.
Approval gates you set. Off until you set them.
policy.default_mode starts at allow_all, so a project that never writes the block runs everything. The reference manifest in the kortix.yaml documentation shows the shape, cut after the agent's tool permissions:
policy:
default_mode: risk
policies:
- match: "helpdesk.delete_*"
action: block
approvals:
ttl: 4h
agents:
support:
description: Answers help desk tickets.
mode: primary
prompt_file: agents/support.md
permission:
bash:
"*": allow
"git push *": ask
default_mode: risk runs a read and holds a write or a delete for approval. The rule under policies blocks every helpdesk delete, whoever asks. approvals.ttl: 4h shortens the default day to four hours, so a stale request cannot wait until Friday. The agent block sets its own tool rules: bash runs, a git push waits for a person, and the narrowest matching rule wins.
Session work reaches main through a change request. Merge is default-deny for agents.
Approvals and merge are both enforced outside the agent. A push to another branch answers 403, and a merge without the project's merge capability answers CR_SELF_MERGE_REFUSED. agent_grant_escalation covers an agent widening its own grants, connector_egress_blocked a connector call to a private address, 401 a wrong credential and 403 a right one still not allowed.
What to check, and where this trail stops
Two limits belong in this answer. A secret delivered to a session as an environment variable is a real value, so code inside the session can read it. Marking a key Enforced gives the computer a stand-in: Kortix swaps in the real value outside the computer, only on requests to the hosts you list, and an echoed value comes back as [REDACTED]. Kortix does not filter outbound network traffic today, so a session's computer can reach the internet directly, for example to install packages. The enforced part is narrower: enforced secrets go only to listed hosts, connector calls never reach private addresses, and a copied session token gets no enforced secret outside its own computer.
Ask these of any product, including this one. Each answer can be checked against a page, not a demo.
| Check | The question | A good answer |
|---|---|---|
| Action names | Is the set closed and published? | A namespace you can filter and alert on |
| Reach | Can you filter, export and stream it? | Prefix filters, an export, a webhook |
| Actor | Does each row name the person? | The human behind the agent, not a role |
| Retention | Is the period written down? | Stated days, plus what the archive holds |
| Refusals | What does a row decline to store? | No raw paths, no secrets, no values |
Kortix meets four of those in the record itself. The fifth, filtering, exporting and streaming, is the Enterprise entitlement. A vendor that says yes to all five without showing you the page has answered none of them.
Two related reads: where the rule that governs an agent lives, and what a private deployment has to record.
Start with the action that would hurt most, and check its name is on the list before you need it. Kortix is free to start and free to self-host.