Every agentic system in production today is an improvisation.
A language model is handed a set of tools. Someone writes orchestration code over a weekend. A logging layer gets bolted on. Permission checks appear after the first near-miss. The whole assembly works until it doesn’t — and when it fails, reconstruction is archaeology. You trace through layers of accumulated convention to find the assumption that broke.
This is the substrate problem. It is not a novel observation. What is novel is how consistently the industry has responded to it by building more on top of the same absent foundation rather than stopping to lay one.
Stratum is our attempt to lay it.
What Stratum is
Stratum is a foundational architecture for the layer between an autonomous AI agent and the world it acts on. It is not a framework for building agents. It is not an orchestration library. It is the substrate layer that makes agentic systems safe, auditable, and composable by design — the thing that needs to exist before more capable systems can be trusted.
The closest analogy is the kernel contract in traditional computing: the agreement between user-space programs and the operating system about what operations are available, how they behave, and what guarantees they carry. That contract made general-purpose computing trustworthy enough to build the world on. Stratum is our attempt to define an analogous contract for agentic computing.
The first target environment is an interceptor layer running on top of an existing OS — not a replacement for one. Every action an autonomous agent takes passes through Stratum before it reaches the world. Stratum classifies the action, decides whether to proceed or escalate to a human, records what happened, and builds a behavioral record over time. The agent is not constrained from taking actions. It is constrained from taking them without oversight proportional to their consequence.
The architecture
Stratum is organized as a monorepo of packages, each responsible for one concern. The dependency direction flows downward: each layer knows about the types below it but never calls functions across that boundary. Cross-layer coordination is the orchestrator’s job — a package we have not yet built, deliberately.
The reversibility engine
The core primitive is the reversibility engine: a pure function that takes an action intent and returns a classification.
An intent is a pre-execution declaration — a typed, structured description of what the agent is about to do, before it does it. A delete_file intent carries the path and the execution context. A kill_process intent carries the process ID. The engine evaluates the intent against a five-class taxonomy:
Class 0 Pure read — no state change → pass through
Class 1 Local reversible write → pass through
Class 2 Remote write, reversible within window → log and proceed
Class 3 Irreversible, low blast radius → escalate
Class 4 Irreversible, high blast radius → escalate
Classification is runtime, not static. A write to an existing file starts at Class 1. It becomes Class 3 if the undo store is disabled. A deletion starts at Class 3. It becomes Class 4 if the target has downstream dependents and is not a temporary path. The classifier reads execution context — the current undo availability, the downstream dependent count, the path’s temporary status — and applies upgrade rules. Crucially, context can only upgrade a class, never downgrade it. This is the upgrade-only constraint: conservative by construction.
The engine is a pure function. No I/O, no side effects, no async. It takes an intent and returns a result. This makes it trivially testable and trivially auditable — we can reason about every possible classification without running the system.
What we learned building it: The initial design had a closed switch statement mapping action types to base classes. As we added domains beyond filesystem operations, this became untenable — every new domain required modifying the core package. We redesigned the classifier as a runtime registry. Domain packages register their own classification rules at plugin construction time. The core engine owns only the execution model: the upgrade-only constraint, the policy map, the reason string. This is the right architecture, and we should have arrived at it sooner.
The audit store
Every action the system takes is recorded. The audit store is append-only, SQLite-backed, and structured for reconstruction: given any point in time, you can replay the event log and arrive at the exact system state that existed then.
Each audit record carries the full classification result — the intent, the class, the policy, the reason string, the human approval if any, the execution outcome, the exit code, and the duration. The record is not a summary; it is a complete provenance tag for every state mutation the agent made.
The audit store is queryable by action type, path prefix, outcome, agent identity, classification class range, and time range. A limit field caps result sets for production deployments where the log has grown large.
The reasoning trace problem: The audit store captures what the agent did. It does not capture what the agent understood. Model internals are opaque, and generated reasoning traces are themselves outputs subject to confabulation — a model can produce a confident, well-structured justification for an action that bears no relationship to the computation that produced it. We do not have a satisfying answer to this. We have the discipline to be honest about the gap rather than paper over it with plausible-sounding logs.
The escalation store
When the classifier returns Class 3 or 4, the action escalates. Escalation in Stratum is a formal primitive, not an ad hoc print to stdout.
The escalation store is also fully append-only, implemented across two tables: one for escalation events, one for resolutions. A resolution never modifies the original escalation row — it inserts a new record. Every state transition is a new fact; nothing is overwritten.
The critical design decision here: escalation is non-blocking. When escalate() is called, the system persists the escalation event immediately and returns { status: 'deferred', escalationId } without waiting for a human response. The agent does not block. The action does not proceed. The escalation sits in the pending queue until a human calls resolve() from wherever they are — a dashboard, a CLI, a mobile notification — hours or days later.
This is the right model for an agentic OS where agents may be running continuously across time zones and time horizons. The alternative — blocking until a human responds — assumes a human is always available and watching. That assumption is not safe to make.
What we learned building it: The original design had EscalationResult declared as a three-way union: approved, rejected, or deferred. In practice, escalate() only ever returns deferred — the approved and rejected states belong to EscalationResponse, which arrives later via resolve(). The union was aspirational rather than descriptive. We narrowed it. The type system is more honest now.
The action surface
The action surface is the boundary between the agent and the world. It is responsible for two things: resolving the execution context needed to classify an action, and executing the action once classification and escalation have cleared.
The action surface is a plugin host. It has no built-in knowledge of any domain — no filesystem operations, no process management, no networking. Domain packages register plugins at startup. Each plugin carries four things: the action types it handles, the handler functions for those types, a context resolver that computes the appropriate execution context, and classification rules that define how the classifier should treat those action types.
This plugin architecture has a specific consequence: adding a new domain to Stratum requires creating a new package. No existing package is modified. The core remains unchanged.
The context resolution problem: Execution context is the data the classifier needs to evaluate an action — the undo availability, the downstream dependent count, the path’s temporary status. For filesystem operations, these fields are computed from OS primitives: file stat, open handle count via lsof, symlink count in configured scan paths. For process operations, different OS primitives apply: child process count via pgrep, connection count via lsof -p, process uptime via /proc/stat on Linux or ps -o etime= on macOS.
We validated the context resolution architecture against two domains — filesystem and process management. The key finding: ActionContext is not a flat type. It is a union of domain-specific types sharing a minimal base:
type BaseActionContext = {
undoAvailable: boolean
downstreamDependents: number
}
type FilesystemContext = BaseActionContext & {
pathIsTemporary: boolean
fileAgeDays: number | null
sizeBytes: number | null
}
type ProcessContext = BaseActionContext & {
isSystemProcess: boolean
uptimeSeconds: number | null
memoryBytes: number | null
}
undoAvailable and downstreamDependents are genuinely universal. Everything else is domain-specific. A network call has no pathIsTemporary. A process signal has no sizeBytes. The union keeps the type system honest; the type guards isFilesystemContext and isProcessContext let the classifier apply domain-specific upgrade rules safely.
The snapshot store
The snapshot store provides the undo layer. Before every mutating action that the snapshot store is configured to capture, the action surface saves the pre-execution state: file contents for writes and deletions, mode strings for chmod operations. The snapshot store is SQLite-backed, uses transactional capture to prevent orphaned snapshot files on process crash, and supports both count-based and size-based eviction — the same model as shell history, where the user configures how deep their undo buffer runs.
Snapshots are not the audit trail. The audit store is the permanent record. Snapshots are recovery artifacts attached to recent actions, evicted as the buffer fills. Deleting a snapshot does not delete the audit record of the action that created it.
The event bus
The event bus is the system’s nervous system. It is implemented as a standalone daemon — stratum-eventd — that runs as a separate process, listens on a Unix domain socket, and persists all events to SQLite before delivering them to subscribers.
Every significant thing that happens in Stratum is an event on the bus: escalation.created, escalation.resolved, action.executed, audit.record. Any component — the orchestrator, the workspace surface, a monitoring dashboard — subscribes to the bus and receives events in real time. Subscribers that were offline when an event fired reconnect and replay from their last-seen event ID. The SQLite backing means nothing is lost across process restarts.
This architecture solves what a design review identified as a critical hole: the escalation store had no mechanism to notify the orchestrator when a human resolved an escalation. The notifier on escalate() told the human a question had arrived. Nothing told the orchestrator the answer had come back. The event bus makes both directions symmetric — escalation.created and escalation.resolved travel the same channel, with the same durability guarantees, to the same subscribers.
What we know is still wrong
We conduct formal design reviews as the project develops. The most recent one produced findings we are working through honestly:
The orchestration layer is the most important unbuilt thing. The packages described above are individually coherent but collectively incomplete. They cannot produce a single useful behavior without an orchestrator that wires classify → escalate → execute → audit into a coherent pipeline. We have deferred this deliberately — building the components first and letting their interfaces reveal the orchestrator’s requirements — but the deferral has a cost. Sequential action workflows are not currently handled: if an agent has a ten-step plan and step three escalates, steps four through ten have no defined behavior. This is the first thing the orchestrator must solve.
The audit store needs a retention strategy. The store is append-only and grows indefinitely. A busy agent processing a hundred actions per minute accumulates 144,000 rows per day. We have added a limit field to the query interface as a mitigation, but there is no archival strategy, no compaction, no pruning. This must be addressed before production deployment.
The downstreamDependents field understates blast radius. For filesystem operations, downstream dependents are computed as open file handles plus symlinks in configured scan paths. This misses hard links, misses application-level imports, and counts file descriptors rather than processes — a single process with three open handles to the same file counts as three. The field is a lower bound on real blast radius and frequently returns zero for consequential deletions. We have renamed it accurately in our own thinking and will reflect this in the documentation.
The architecture has only been validated against easy domains. Filesystem operations are the most favorable possible domain for demonstrating this architecture — enumerable operations, content-level snapshots, clear reversibility gradients. We extended to process management as a stress test and the architecture held. But network operations, IPC, and container management remain untested, and the context resolution fields that work cleanly for filesystem and process semantics may not generalize without further extension.
Where we are
The codebase has eight packages:
@stratum/reversibility— the open classifier registry, complete@stratum/audit— the append-only event store, complete@stratum/escalation— the non-blocking escalation primitive, complete@stratum/action-surface— the domain-agnostic plugin host, complete@stratum/action-surface-fs— the filesystem domain plugin, complete@stratum/action-surface-process— the process management domain plugin, complete@stratum/events— the durable event bus daemon, in active development@stratum/workspace— the human-visible execution surface, stub
The test suite runs 350+ tests across all packages using real filesystem and process I/O. We do not mock the filesystem in tests. Mock/reality divergence is a real failure mode and we prefer slower, noisier, honest tests.
What we do not have: an orchestrator, a working workspace, capability tokens, an agent identity model, or a trust model that reads from the behavioral record to expand permissions over time. These are not forgotten — they are listed explicitly in CLAUDE.md as the next design problems to solve. The orchestrator comes first because everything else depends on it.
Why any of this matters
The question we return to most often is whether this level of rigor is actually necessary — whether the industry will simply route around safety constraints the way it routes around most friction, and whether a carefully designed substrate will be displaced by a faster, looser, more capable one.
We think the answer depends on what happens when the first agentic system causes a serious, public, irreversible failure. Not the failures that happen quietly in development environments. The one that lands on the front page. That failure will almost certainly be a classification failure — an action that should have been escalated, wasn’t, and couldn’t be undone. And when it happens, the industry will reach for exactly the kind of infrastructure Stratum is building.
We would rather that infrastructure exist before the failure than after it.
The repository is github.com/mihok-labs/stratum. The README explains the full architecture. The code explains the rest. If you find something wrong, open an issue.
Mihok Labs is an independent research and engineering organisation working on foundational architecture for agentic computing.