Field notes

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.

The Kortix Team
Kortix··7 min read

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 keepsWhat it refuses
One named action from the published setA free-text description of the call
The route template of the requestThe raw path and any token inside it
The person the agent acted forAn unnamed service identity
The refusal, with its reason codeSuccesses only
api.route.unmatched for a dead endSilence 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.

CheckThe questionA good answer
Action namesIs the set closed and published?A namespace you can filter and alert on
ReachCan you filter, export and stream it?Prefix filters, an export, a webhook
ActorDoes each row name the person?The human behind the agent, not a role
RetentionIs the period written down?Stated days, plus what the archive holds
RefusalsWhat 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.

Own your company’s AI.Start today, in our cloud or yours.

Start for free

Open source · SOC 2 Type I · SAML SSO and SCIM · Any model, your keys