Status and Roadmap - joeylking/agent-runtime GitHub Wiki

Status and Roadmap

This page is what is released, newest first. The forward plan has its own page: Roadmap, which mirrors docs/roadmap.md: its eight items and, since v0.4.0, a Direction section with items 9 to 12. Items 1, 2, and 8 shipped earlier. Items 3 to 6 shipped together in v0.3.1; item 5 meets its done-when, and items 3, 4, and 6 were adopted by both consumers on 2026-10-03. Item 7 is ongoing. Item 9, the gate, shipped in v0.4.0, and its first form outside Go, the MCP proxy, shipped as proxy/v0.1.0. Item 10, re-checkable decisions, shipped in v0.5.0 with proxy/v0.2.0 and export/otel/v0.1.2. Items 11 and 12 are intentions, not promises, in that order, with item 12 as research.

Released: v0.5.0, with proxy/v0.2.0 and export/otel/v0.1.2

The current release, published 2026-10-08: core v0.5.0, with proxy at v0.2.0 and export/otel at v0.1.2, both requiring core v0.5.0. mcp, providers/ollama, providers/anthropic, providers/openai, and bench did not change and are re-pinned only where their go.mod was updated, without a new tag. It is roadmap item 10: it records what a decision needs to be decided again, chains the events, and replays a finished run through a policy. An upgrade needs an operator step: stop every older process and operator binary that has the database open before the first open, because migration 6 chains the events, an older binary's events would carry no hash, a v0.4.0 binary refuses a migrated database, and the migration hashes every stored event once. The minor bump marks that migration and one behaviour change, below. See Upgrading to v0.5 and Re-checking a Run.

  • Each decision records what it can be checked against. A tool_call step records the hash of its tool's spec (Step.SpecHash), the spec stored once in tool_specs; a policy implementing IdentifiedPolicy has its identity recorded on each step it evaluated; and step.policy carries the spec the policy was handed and a view of the run it saw.
  • The events are a hash chain. Each event's hash covers the previous event's and its own fields as stored, across every run, computed in the transaction that writes it. Store.VerifyEvents, ChainHead, and EventHash read it, and agentrt verify [-from N] [-to N] [-head SEQ:HASH] checks it and exits 1 on a break. The chain proves integrity only against a head kept where whoever can write the database cannot reach; export.Follower.Head gives one to keep.
  • agentrt.Recheck replays each recorded policy evaluation through a policy, rebuilt as the policy saw it, and reports same, different, not_a_policy_evaluation, or not_recheckable, with Same, Rechecked, and Complete. trace.WriteRecheck renders it. A run recorded before v0.5.0 is not re-checkable. A policy that reads outside state cannot be re-checked exactly.
  • testkit.Recheck, RecheckDiffers, RecheckPartial, and Scenario.Recheck do this in a consumer's tests, and fail when nothing was re-checked. See Testing a Consumer.
  • agentrt-proxy recheck (proxy/v0.2.0) re-checks the proxy's recorded calls under the configuration it has now, and the proxy's policy has an identity, Config.PolicyID. The proxy's rules applied before its policy are not re-checked. See MCP Proxy.
  • export/otel/v0.1.2 carries agentrt.policy.id and agentrt.policy.spec_hash.
  • examples/recheck shows a run re-checked under a stricter policy and an altered event found by the chain check, with no model.
  • Behaviour change: a policy is handed the arguments and its tool's schema in PolicyJSON form on every path. Keys sorted, no insignificant whitespace, numbers as written, no HTML escaping. The loop and a resume used to hand different bytes. A policy that unmarshals is unaffected; one that matched whitespace, key order, or HTML escapes would notice; one that copies the arguments into its capability or presentation now stores them with keys sorted, and the approval's hash is unchanged. See Upgrading to v0.5.

Re-checkable decisions: the record

Capability Status Reference
Each distinct tool spec is stored once in tool_specs under the hex SHA-256 of its canonical JSON, every field included; each tool_call step naming a registered tool records the hash with its decision, valid or not, and its step.policy event names the same spec; run.created lists every registered tool's hash in name order; Store.ToolSpec reads a spec back and refuses one whose stored JSON no longer hashes Verified record.go, TestToolSpec_StoredOnceAndReferencedByEachStep
A tool registered again with another description, timeout, or terminal flag is another spec, and both stay readable Verified TestToolSpec_ChangedSpecIsAnotherHash
A policy implementing IdentifiedPolicy has its identity recorded on each step it evaluated and in that step.policy; a step it never saw, and a plain policy, record none; an identity over 256 bytes, not UTF-8, or holding a control character is refused by NewDriver and NewGate Verified TestPolicyID_RecordedOnStepAndEvent, TestPolicyID_MalformedIsRefused
step.policy carries what the policy saw of the run: the run rebuilt from run.created and its view equals the RunView's run field by field, and its step and approval counts are the run's first ones, in a model-backed loop, on resume, and through a gate Verified TestPolicyView_IsWhatThePolicySawInTheLoop, TestPolicyView_IsWhatThePolicySawOnResume, TestPolicyView_IsWhatThePolicySawThroughAGate
Every event is chained to the previous one by seq across runs, in its writing transaction; the encoding (netstrings, SHA-256) is pinned by a literal and recomputed independently Verified chain.go, TestEventHash_Golden, TestVerifyEvents_IntactChainAcrossRuns
VerifyEvents reports the first event that does not chain: an altered payload or time at its seq, a deleted event at the next seq, two swapped events at the first; a chain cut at its end verifies, and only a kept head (ChainHead, EventHash) shows it Verified TestVerifyEvents_FindsTheFirstAlteredEvent
A field past a page is read and hashed in chunks of MaxPageText, and a change past the first chunk is found Verified TestVerifyEvents_ReadsLongFieldsInChunks
Many runs written at once from several stores on one file leave one intact chain Verified TestVerifyEvents_ConcurrentWritersKeepTheChain
The fixtures that emit every event type, a pause and resume, a crashed process taken over, a cancel with a late tool outcome, a model-backed run with a retry, form one intact chain, and the follower delivers all of it Verified export/otel, TestFixtures_EventChainIsIntact
Migration 6 adds tool_specs, steps.spec_hash and policy_id, and events.hash, backfilling the hashes in seq order; a v0.4.0 database is refused by OpenExisting until migrated, then has its runs intact, the same hashes on every migration of the same events, an intact chain, and a waiting run that resumes onto it recording the spec it ran with; released migrations are pinned Verified TestStore_V040DatabaseMigratesWithChainedEvents, TestStore_ReleasedMigrationsAreUnchanged
agentrt verify [-from N] [-to N] [-head SEQ:HASH] prints the head and "intact" with the last checked seq and hash, or the first break with the hash expected and found, in text or JSON, exits 0, 1 on a break, 2 on bad arguments before opening anything, and does not change the file Verified cmd/agentrt, TestVerify_IntactChainExitsZero, TestVerify_BrokenChainExitsOne, TestVerify_BadArgumentsAreUsage
A database cut at its end verifies on its own, and a head kept from before the cut fails it: -head at a seq no longer reached, -to that seq, and a kept head whose event has another hash exit 1; a kept head inside the chain matches Verified TestVerify_CutDatabaseFailsAKeptHead
The JSON Lines record of an event read back from the store is byte for byte as before: Event has no hash field Verified TestJSONL_RecordIsByteStable
Recording specs, identities, views, and hashes reads no clock Verified TestDriver_ClockReadingsAreStable
export/otel carries policy_id and spec_hash as agentrt.policy.id and agentrt.policy.spec_hash Verified TestExporter_RunAndStepSpans

Re-checkable decisions: re-checking a run

Capability Status Reference
Recheck under the policy a run ran with reports every evaluation the same: an allow, a deny, and an approval granted and resumed, the resume a line of its own, the identity unchanged, and the recorded approval hash the stored approval's Verified recheck.go, TestRecheck_SamePolicyIsSame
Under a stricter policy the report names exactly the evaluations that differ and what they would have become Verified TestRecheck_StricterPolicyNamesTheStepsThatDiffer
A policy whose approval presentation changed differs on each require_approval by the approval's hash while the outcome matches; a changed reason is never a difference Verified TestRecheck_ChangedPresentationDiffersByHash
Recorded specs against current ones: a tool whose description changed between runs re-checks the same against what each run recorded, and today's spec makes the earlier run's approvals other approvals; a tool no longer registered, or arguments today's schema refuses, differ Verified TestRecheck_CurrentSpecsAgainstRecordedSpecs
A policy error is a difference; a run not terminal is re-checked up to its current state with a note; a missing run is ErrNotFound; an identity NewDriver refuses is refused Verified TestRecheck_PolicyErrorAndRunNotTerminal
A policy error the run met, in the loop or on resume, recorded no evaluation and is listed at its step.failed as not re-checkable, "policy failed; no evaluation recorded", and the report is not Complete Verified TestRecheck_PolicyErrorIsListed
A decision refused as invalid re-checks the same when the policy returns it again for the same reason, with the reason, and differs when the policy now returns a valid one Verified TestRecheck_InvalidDecisionRefusedAlikeIsSame
PolicyJSON sorts keys, drops whitespace, keeps numbers' literal text, escapes only what JSON requires, has the canonical form of its input, and refuses what CanonicalJSON refuses Verified canonical.go, TestPolicyJSON_Form
A policy is handed the arguments and the schema in PolicyJSON form, the same bytes from the agent's pretty input and from the stored, escaped decision and canonical spec, for &, <, >, U+2028, non-ASCII, nested keys out of order, and numbers such as 1.50 and 1e3 Verified TestPolicyJSON_SameFromRawAndStored
A policy that denies && by its bytes decides the same live and on a re-check; a pretty-printed schema is handed alike live and on a re-check, recorded or current Verified TestRecheck_ByteReadingPolicyDecidesAlike, TestRecheck_PrettySchemaHandedAlike
One request is handed in the same bytes in the loop and on resume, and through a gate on Propose and Attach Verified TestPolicy_LoopAndResumeHandedTheSameBytes, TestPolicy_GateProposeAndAttachHandedTheSameBytes
The approval asked for a call with raw, pretty arguments has the hash it had before the policy form, pinned Verified TestApprovalHash_UnchangedByThePolicyForm
An approval whose capability is longer than MaxPageText is handed to the re-checked policy whole, so a policy that allows under the whole grant decides the same Verified TestRecheck_ApprovalLongerThanAPageIsReadWhole
Resuming the first step hands the policy no steps as nil, as the loop and a re-check do; the rebuilt views equal the ones handed field by field and as JSON Verified TestRecheck_ResumeAtTheFirstStepHasNoSteps
The view handed to the policy in a re-check is, field by field, the one it was handed, and the request the same, in a model-backed loop, on resume, and through a gate's Propose and Attach Verified TestRecheck_RebuildsTheViewOfTheLoop, TestRecheck_RebuildsTheViewOnResume, TestRecheck_RebuildsTheViewThroughAGate
A v0.4.0 database is re-checked rather than refused: its step.policy events are not re-checkable, a run created then says so, and an evaluation made after the migration is re-checked Verified TestRecheck_PreMigrationRunIsNotRecheckable
An interrupted side effect's pause is reported as no policy evaluation, between the step's first evaluation and the one on resume Verified testkit, TestRecheck_InterruptedSideEffectPauseIsNotAPolicyEvaluation
The text and JSON renderings are pinned; the text is one sanitized line per evaluation, differences first Verified TestRecheck_RenderingsArePinned, TestWriteRecheck_OneSanitizedLinePerEvaluationDifferencesFirst, ExampleRecheck
SideEffectPolicy does not implement IdentifiedPolicy Verified TestSideEffectPolicy_IsNotIdentified
testkit.Recheck passes a policy that decides from its input and fails one that reads outside state, through Scenario.Recheck; testkit.RecheckDiffers asserts the steps a second policy changes and reports a wrong expectation with the report Verified TestRecheck_ScenarioPolicyThatDecidesFromItsInputPasses, TestRecheck_PolicyThatReadsOutsideStateFails, TestRecheckDiffers_NamesTheStepsASecondPolicyChanges
testkit.Recheck, RecheckDiffers, and Scenario.Recheck fail a re-check that proves nothing: a run recorded before v0.5.0, one whose only evaluation met a policy error, and one that asked the policy nothing; RecheckPartial passes them Verified TestRecheck_FailsWhenNothingWasRechecked
export.Follower.Head is the seq delivered up to and its stored hash, which VerifyEvents up to it finds intact, unmoved by events not yet delivered Verified TestFollower_HeadIsTheChainAsFarAsDelivered
The demonstration re-checks a scripted run under a stricter policy, which names the step it would have stopped, and the chain check finds an event altered in a copy Verified examples/recheck, TestRun_StricterPolicyStopsThePublishAndVerifyFindsTheAlteredEvent
The proxy's calls re-check the same under the configuration they ran with, identified by it; under a policy now denying payments the payment's two decisions are named and nothing else; -run, a limit, and another session select runs Verified proxy, TestRecheck_ChangedPolicyNamesTheCallsNowDenied
The proxy's policy identity covers the policy map and each rule's side effect, outcome, fixed values, and rename, and nothing else Verified TestConfig_PolicyIDCoversWhatThePolicyDecidesFrom
With the tools loaded now, a reworded payment description makes its approvals other approvals while the recorded specs re-check the same and the identity stays Verified TestRecheck_CurrentToolsAfterADescriptionChange
A re-run of a payment whose outcome is unknown re-checks the same, the attempt it re-ran read from its interruption approval Verified TestRecheck_ReRunOfAnUnknownOutcomeIsTheSame
agentrt-proxy recheck exits 0 when every run is the same, 1 when one differs or the run is missing, 2 on a usage error; prints text or JSON with a summary of decisions re-checked and skipped and what the re-check covers; -current-tools reports the load on stderr Verified proxy/internal/cli, TestCLI_Recheck
agentrt-proxy recheck exits 1 when no run of the session was found and when no decision could be re-checked Verified TestCLI_RecheckOfNothingFails

Released: v0.4.0, with mcp/v0.2.1, export/otel/v0.1.1, and proxy/v0.1.0

The release before it, published 2026-10-05: core v0.4.0, with mcp at v0.2.1, export/otel at v0.1.1, and the new nested module proxy at v0.1.0, its first tag. All three require core v0.4.0, and proxy also requires mcp v0.2.1. providers/ollama, providers/anthropic, providers/openai, and bench are re-pinned to it on main without a new tag, because their code did not change. Everything in the release is additive, except that a number past the new bound is refused where consumer JSON enters. An upgrade from v0.3.x needs nothing beyond re-pinning, and there is no migration or operator step. See Upgrading to v0.4.

  • The gate (roadmap item 9). Gate, Session, OpenStep, and Verdict give the runtime's controls to a loop the runtime does not own, and the Driver runs on the same code. It also brought Decision.Origin, Session.Input, a GrantTTL re-check at Execute, and testkit.Scenario.Loop with two new crash points. The gate is the one addition justified by outside research and not by a consumer. See The Gate and decision 7.
  • The MCP proxy (the first form of the gate outside Go). agentrt-proxy is a local MCP proxy over stdio that routes an MCP host's tool calls through a gate, one run per call. It is a small, scoped experiment, and what it cannot govern is stated first. See MCP Proxy and decision 8.
  • agentrt.CanonicalJSON returns the form the runtime hashes. It is the existing encoder, exported, and every hash is unchanged (TestContentHash_Golden).
  • mcp: ReadOwned, OwnedPrivately, ReadManifest, ReadRules, and FileRule, the operator-file readers examples/mcp has, added to the module so the proxy reads the same files under the same rule. examples/mcp is unchanged and keeps its own copies.
  • Security fix: a model-chosen number could crash the process, in every release through v0.3.2. Numbers whose literal is longer than 10,000 characters or whose exponent is beyond plus or minus 10,000 are refused where consumer JSON enters, and a panic inside validation is a refusal. See Upgrading to v0.4 and Security Model.
  • Security fix: a grant is judged against Limits.GrantTTL again at Execute. This was never in a release, because the gate is new, and the Driver is unaffected.

The gate

Capability Status Reference
A loop written outside the runtime, driven through a Gate, leaves the same rows, events, payloads, transactions, and clock readings as the Driver running the same decisions; the Driver runs on the gate's code Verified gate.go, loop.go, TestGate_ExternalLoopMatchesTheDriver
NewGate checks what NewDriver checks, without an agent Verified TestGate_ConfigIsValidated
A denial is an observation and the session goes on; an abort ends the run and the session refuses every call after it Verified TestGate_DenyContinuesAndAbortEnds
A request that needs approval pauses the run and ends the session; after approval Attach returns the step allowed and Execute runs exactly the recorded request, whatever the caller does to the verdict it was shown Verified TestGate_ApprovedStepExecutesTheRecordedRequest
Policy is re-evaluated at Attach: a different required approval pauses again and nothing runs, and rejecting it cancels the run Verified TestGate_ChangedPolicyRepausesAndRejectCancels
The step, consecutive failure, and loop limits fire in Step, which returns no step and no error Verified TestGate_LimitsFireThroughStep
The call, token, and cost limits fire in the step's ModelCaller before anything is sent, and Fail ends the run with the limit's reason Verified TestGate_ModelLimitsFireThroughTheStepsModel
A session refuses, with typed errors and without changing anything, a second open step, an Execute with no allowed verdict, a second Execute, a second decision, and every call after Close Verified TestGate_MisuseIsRefused
A session whose lease was lost refuses every call with ErrLeaseLost and starts nothing Verified TestGate_CallsAfterTheLeaseIsLostAreRefused
While one session holds a run another Begin or Attach of it is refused and changes nothing; after Close the run is free without waiting for the lease Verified TestGate_SecondSessionIsRefused
A session abandoned between an allowed verdict and Execute runs nothing: a fresh gate marks the step interrupted, and the abandoned step cannot execute Verified TestGate_AbandonedAfterAllowedRunsNothing
A session abandoned during Execute pauses the run as interrupted_side_effect for a fresh gate with no Reconcile; the tool runs again only when an operator approves, and the abandoned session cannot record over the run Verified TestGate_AbandonedDuringExecutePausesForAnOperator
An approved step Attach left allowed and the caller abandoned is still approved and the run still WAITING with nothing written; the next Attach runs it once Verified TestGate_AbandonedApprovedStepStaysApproved
A gate with every session over, ended or closed, holds no goroutine Verified TestGate_ClosedSessionsLeaveNoGoroutine
Limits.GrantTTL is judged again at Execute: a grant that expired while the caller waited cancels the run as approval_expired, executes nothing, and ends the session with ErrApprovalExpired; within the TTL, or with none, it executes however long the wait Verified TestGate_GrantTTLHoldsUntilExecute
Decision.Origin is recorded with the decision and changes nothing else: a decision without one encodes as before, and the approval hash is the same with or without it Verified TestDecision_OriginIsRecordedAndChangesNothingElse
An unknown origin is an invalid decision through a gate and a Driver alike; each known origin is accepted Verified TestDecision_UnknownOriginIsInvalid
export/otel carries a decision's origin as agentrt.decision.origin when the payload names one, and omits it otherwise Verified export/otel/otel.go, TestExporter_DecisionOrigin
Session.Input while a step is open is exactly what a Driver's agent is handed at that step, and reads no clock Verified TestGate_InputMatchesTheAgentsStepInput
Between steps Session.Input is the run as the session left it and every recorded step; after the first call none loads the steps again Verified TestGate_InputAtEveryPointOfASession
What Session.Input returns is the caller's copy: writing into it changes neither the session's state nor the record Verified TestGate_InputIsACopy
testkit.Scenario.Loop runs the crash harness over a caller's loop on a gate through every crash point, and every invariant of the run's record holds Verified testkit/testkit.go, TestRun_GateLoopKeepsTheContractAtEveryCrash
Crash point AfterAttachAllowed abandons an approved step after Attach; the next Attach runs the grant once Verified TestRun_GateLoopAbandonedAfterAttachRunsTheGrantOnce
Crash point AfterAllowed stops a Driver between its policy and the tool start, and runs nothing Verified TestRun_DriverStopsAfterAllowed
A broken loop is reported: one that ignores the approved step, one that stops mid-step, and an error the loop returns Verified TestRun_BrokenGateLoopsAreReported

The MCP proxy

Capability Status Reference
The host is offered only the classified, pinned tools under their registered names, with the operator's descriptions and the schemas without fixed parameters, and the status tool; no output schemas; an unclassified tool is absent and reported Verified proxy/proxy.go, TestProxy_ListsOnlyClassifiedToolsAndTheStatusTool
A tool whose description changed since the pin is refused and not offered Verified TestProxy_ChangedDescriptionRefusesTheTool
An allowed read runs once and answers the server's result Verified TestProxy_AllowedReadReturnsContent
A denied class answers DENIED and the server is never called; arguments the schema refuses answer INVALID with the runtime's reason Verified TestProxy_DeniedAndInvalidRunNothing
A call that needs approval answers PENDING_APPROVAL and runs nothing; approved through the path agentrt uses, the identical call runs it once and answers the result; a third identical call within the window is DUPLICATE and runs nothing Verified TestProxy_ApprovalRoundTrip
With the repeat window off, the identical call after an executed one asks for a new approval Verified TestProxy_RepeatWindowZeroAsksAgain
A call approved within the hold runs and answers in the same call, with progress notifications to a request that carried a token Verified TestProxy_ApprovedWithinTheHoldCompletesInOneCall
A request is keyed on agentrt.CanonicalJSON, the form approvals are hashed over: key order, whitespace, and a string's escaping do not make a new request; a changed string, a changed number, or a number written another way (1250.5, 1250.50) does; arguments the runtime refuses as JSON (a repeated key, an unpaired surrogate, invalid UTF-8, a number past the bound) have no key, are INVALID, and never collect another request's approval Verified proxy/key.go, canonical.go, TestProxy_CanonicalMatching, TestRequestKey_CanonicalByLiteral, TestRequestKey_NoCollisions, TestProxy_ArgumentsTheRuntimeRefusesNeverCollect, TestCanonicalJSON_IsTheHashedForm
Several approvals wait at once and are collected independently; past max_pending a new one is BLOCKED without an approval Verified TestProxy_SeveralApprovalsAtOnceAndTheCap
A rejected request answers REJECTED with the note and asks for nothing new within the window; an expired one answers EXPIRED likewise Verified TestProxy_RejectedIsNotAskedAgain, TestProxy_ExpiredIsNotAskedAgain
A grant past grant_ttl by the proxy's clock executes nothing Verified TestProxy_StaleGrantExecutesNothing
Policy is evaluated again at collection: a class denied since the approval runs nothing Verified TestProxy_PolicyChangedToDenyBeforeCollection
The operator's fixed values are in the approval's capability: a changed value asks for a new approval instead of executing Verified proxy/policy.go, TestProxy_ChangedFixedValueAsksAgain
An upstream isError result is an error result carrying the recorded text; structured content reaches the host Verified TestProxy_UpstreamErrorAndStructuredResult
A host that cancels a call does not stop it: the outcome is recorded and the identical call is DUPLICATE with it Verified TestProxy_HostCancelLetsTheCallFinish
Each outcome's run carries the gate's events in order, the tool call of origin model and the closing decision of origin operator, and ends Verified TestProxy_RunsRecordWhatHappened
The status tool reports what is pending, and for an approval id pending, approved and awaiting collection with the instruction to call again, approved and executed only when its step executed and with what its step recorded (succeeded; the server reported a failure, with the error; executed with its result unrecorded; or outcome unknown, naming the approval that now asks whether it runs again), approved but not executed with why (denied at collection, cancelled), rejected with the note, and expired; it executes nothing and is not recorded as a run Verified proxy/status.go, TestProxy_StatusToolReportsAndNeverExecutes, TestProxy_StatusSaysWhetherAnApprovedRequestExecuted, TestUnknown_StatusSaysWhatCameOfAnApprovedRequest
No model-visible text, outcome texts, the status tool's answers, the descriptions the proxy writes, or its server name, contains "agentrt", the database path, the configuration path, or a run id Verified TestProxy_ModelVisibleTextNamesNoOperatorPath
A row in the index without a run means nothing started, and the next identical call begins that run under the row's id; of two calls that try, one begins it; a call that stalled before its Begin leaves no run behind; such a row is settled when a scan first finds it and unsettled when its run begins Verified proxy/index.go, TestIndex_RowWithoutARunIsBegunUnderItsID, TestIndex_OneCallBeginsARow, TestIndex_ALateBeginLeavesNoRunBehind, TestIndex_RowWithoutARunIsSettledWhenFound
A call abandoned before its tool ran is ended with its reason and the request runs in a fresh run; for a mutating call, abandoned after the policy allowed it or its lease lost before it executed, that is no attempt: no operator is asked and the tool is not blocked Verified proxy/call.go lastAttempt, TestProxy_AbandonedCallIsEndedAndRunFresh, TestUnknown_CutOffBeforeExecutingIsNoAttempt
The configuration refuses unknown fields, an unsafe session name, and a status tool name an upstream tool takes; the configuration, a manifest, and the database are refused when group or others can write them Verified proxy/config.go, TestConfig_RefusesUnknownFields, TestConfig_RefusesAnUnsafeSessionName, TestConfig_RefusesFilesOthersCanWrite, TestProxy_StatusToolNameClashRefused
A repeated key in the configuration or a manifest, a lease_ttl under 3s, and a database that is a symbolic link are refused; the database is checked again after the store opens it Verified proxy/config.go, proxy/proxy.go checkDatabase, TestConfig_RefusesWhatCouldReadTwoWays
A stdio server whose arguments name the configuration, a manifest, the database, or a directory holding one, in the spellings the check reads (a link, a trailing slash, a relative path, --root=, --root:, -r/path, file://, ~ and ~/..., a different case on a case-insensitive filesystem), is refused at startup unless skip_path_check is set; arguments that are not paths are accepted. A best-effort guard against a mistake, not a boundary Verified proxy/config.go checkReach, TestConfig_RefusesAServerThatReachesTheProxysFiles, TestConfig_ReachCheckReadsCommonSpellings
A mutating call whose outcome is unknown, a timeout or a failure with no answer from the server, is answered UNKNOWN_OUTCOME and waits in the same run on an interrupted_side_effect approval naming the attempt; the identical call inside and long after the window runs nothing, other mutating calls to the tool are BLOCKED, and only an approval runs it again; a rejection unblocks the tool, and the identical call later asks an operator again rather than run on the policy; an error the server returned is a known failure, a duplicate within the window, and runs again after it Verified proxy/call.go rerun, unresolved, TestUnknown_TimeoutAfterTheEffectWaitsForAnOperator, TestUnknown_RejectionUnblocksAndAnIdenticalCallAsksAgain, TestUnknown_ServerErrorIsAKnownFailure, TestUnknown_ClassifiesRecordedErrors
An unknown outcome left with nothing waiting, a re-run that met the run's step limit, a run stopped after the attempt was recorded and before the re-run was asked, or an approval that expired, keeps the tool BLOCKED: the question is asked again at once in a fresh run, or by the next mutating call to the tool, which is blocked on that approval, so no tool is blocked with nothing for an operator to act on Verified proxy/call.go unknownBlock, reopen, TestUnknown_StepLimitKeepsTheToolBlocked, TestUnknown_CrashBeforeTheReRunKeepsTheToolBlocked, TestUnknown_ExpiryAsksAgainRatherThanUnblocks
A mutating call whose server answered with content the runtime refuses is answered UNRECORDED, executed with its result unrecorded, not failed; no operator is asked, and the identical call within the window is DUPLICATE Verified proxy/call.go unrecorded, TestUnknown_UnrecordedResultCountsAsExecuted
Server text in the proxy's prose, an error result and a duplicate's quoted outcome, is escaped with trace.Sanitize; a result returned as the call's result is passed back as recorded Verified proxy/text.go clean, TestProxy_ServerTextInProseIsEscaped
Defaults and relative paths are as documented Verified TestConfig_DefaultsAndRelativePaths
pin writes each manifest 0600 and prints the server's hints; check prints the load report and fails when a server registers nothing; the help states the scope Verified proxy/internal/cli/cli.go, TestCLI_PinAndCheck, TestCLI_HelpStatesTheScope
A second proxy process collects an approval granted while the first was gone Verified TestProcess_RestartCollectsAnApprovalGrantedWhileItWasGone
Two proxy processes on one session run an identical concurrent call once and different calls side by side Verified TestProcess_TwoProxiesOnOneSession
A proxy killed inside a mutating call: the identical call is IN_PROGRESS while the lease is live, then INTERRUPTED on an interrupted_side_effect approval; other calls to the tool are BLOCKED and reads work; approved, the identical call runs it again once; rejected, nothing runs, the tool is unblocked, and after the window the identical call asks an operator again Verified TestProcess_CrashThenApproveRunsItAgainOnce, TestProcess_CrashThenRejectRunsNothingAndUnblocks, TestProcess_CrashThenRejectAsksAgainAfterTheWindow, TestProcess_CrashOfAnApprovedPaymentAsksAgain
A host that closes stdin mid-call: the call finishes, is recorded, and is not interrupted Verified TestProcess_StdinCloseMidCallFinishesTheStep
The demonstration runs with no model: an allowed read, an approval collected once, a crash that waits for the operator, and one execution with the proxy against two without Verified proxy/example/main.go, TestDemo_RunsEveryStep
mcp.ReadRules, ReadManifest, and ReadOwned read the operator's files only when they are the user's own and nobody else can write them, and refuse unknown fields Verified mcp/file.go, TestReadRules_ConvertsTheOperatorsFormat, TestReadRules_RefusesWhatItDoesNotKnow, TestReadOwned_RefusesAFileOthersCanWrite

The number bound has its own row in docs/status.md under the security sweep: TestCheckJSON_RefusesNumbersPastTheBound, TestDriver_HugeNumbersAreRefusedWhereTheyEnter, TestGate_HugeNumberIsAnInvalidDecision, TestCheckArgs_RefusesNumbersPastTheBound, and TestValidate_APanicInTheLibraryIsARefusal.

Released: v0.3.2, with bench/v0.1.1

Published 2026-10-03, both additive patches found while repo-steward and casework adopted v0.3.1. Added: approver.RunReader, implemented by approver.Local, so a channel can show the run it is about to cancel without opening the store; testkit.StandIn, a tool of a class none of a consumer's tools has, for Never. Fixed: bench's means-across-repetitions table shows mixed reached states the way it shows mixed outcomes, pinned by a golden test; doc comments on approver.Decision, approver.Open, approver.Local.Now, bench.File.Commit, export.Follower.Follow, and testkit.Result.Resumes now say what the code does. export/otel v0.1.0 and the v0.2.0 modules are unaffected. See the Essays published the same day.

Released: v0.3.1, with bench/v0.1.0 and export/otel/v0.1.0

Published 2026-10-02: core v0.3.1, with two new nested modules, bench and export/otel, each at v0.1.0. mcp and the three provider modules stay at v0.2.0, which pins v0.3.0 and is unaffected. The release is additive: nothing a v0.3.0 consumer calls changes, and no migration or operator step is needed. Roadmap items 3 to 6 shipped here, and both consumers adopted them on 2026-10-03.

  • Scoring an agent that acts: the bench module (item 3). A consumer declares a closed set of outcomes, each classed success, safe non-success, or unsafe, and scores every trial into exactly one. Every count carries its denominator, an excluded trial is in none, one unsafe outcome disqualifies a mode, and Compare refuses results from different commits unless the caller allows it, which the output then says. The result file refuses anything that does not follow from its trials and encodes byte for byte. It reproduces repo-steward's nine result files and casework's published evaluation exactly. It has no dependencies, so it requires no core version. See Measuring Agents.
  • Testing a consumer without a model: the testkit package (item 4). testkit.Run crashes the loop at a chosen point, resumes it in a fresh driver after the lease expires, and asserts the interruption contract. It also provides a policy conformance table (CheckPolicy, Never), schema fuzzing of tool arguments, and a render equivalence check. See Testing a Consumer.
  • Watching runs in a tool you already have: the export package and the export/otel module (item 5). export.Follower delivers a database's events in commit order from a cursor, read-only and from a separate process, with a JSON Lines sink. export/otel turns them into OpenTelemetry spans with ids derived from the run's own, and export/otel/example sends a database's runs to the collector the OTEL_* environment names. Checked against a stock collector on 2026-10-02. See Exporting Events.
  • Approving from a chat bot or a browser: the approver and approver/webhook packages (item 6). approver.Approver shows an approval, approves, rejects, or cancels the run, and approver/webhook serves it over HTTP behind an HMAC signature, a timestamp window, replay refusal, a rate limit, and a body bound. examples/approver runs the whole exchange with no model and no network beyond loopback. repo-steward's publication approval was granted through it and the hash still bound on resume. See Approval Channels.
  • Core additions the new packages are built on, all additive. Store.Lease, Store.ListEventsAfter, Store.ApprovalReason, ErrApprovalExpired, the Operator type, and CompileTool, ToolSchema.Check, and CheckArgs. See Persistence and Inspection, Approvals and Resume, and Writing Tools.
  • Fixes. approve </dev/null now refuses at exit 2 instead of prompting and declining at exit 1, and reaching the end of input at a real terminal refuses the same way. cancel on a finished run names the run's status instead of reporting an approval already decided, because view now matches the core's sentinel errors with errors.Is instead of searching their messages. show's nested JSON indents consistently, and agentrt's usage text lists -limit and -offset. See The Operator Command.
  • Documentation corrections. The README and changelog now name the third case where v0.3.0 changed what render produces: the recent-results window counts rendered steps, so a run with a step interrupted while deciding inside the window renders differently. The provider package docs and a docs/status.md row now say 408, 409, and 425 are transient along with 429 and 5xx, as the code has since v0.3.0.

docs/status.md lists every capability with the test that verifies it, under "Item 3" to "Item 6" and "Core additions for items 3 to 6".

Released: v0.3.0, with the nested modules at v0.2.0

The release before v0.3.1, published 2026-10-01: core v0.3.0, with providers/ollama, providers/anthropic, providers/openai, and mcp at v0.2.0, each requiring core v0.3.0. No roadmap item moved; this release is an audit and a four-position security review of v0.2.1, and several of its changes are ones a consumer or an operator meets on the first run. Read Upgrading to v0.3 before v0.3.0 opens an existing database.

  • Integrity, from the audit. Run and step transitions are compare-and-set, so two resumers execute an approved request once and a cancel is never overwritten; a step and the run it ends commit together, as do a resume and the tool start it approved; only a step's latest approval can be a grant; an approved request is checked against the tool's current schema on resume; nothing stored or hashed is replaced by a stand-in; a row that will not decode no longer wedges its run; migrations run in one immediate transaction; lists follow insertion order.
  • The run lease, migration 5. A running run belongs to the driver executing it; another process's Resume gets ErrRunLeased; a loop that loses its lease starts nothing more. ADR 5.
  • Interrupted side effects wait for an operator when the consumer has no reconciliation, on an interrupted_side_effect approval. ADR 6.
  • Fail-closed handling throughout, from the security review. The database is owner-only and refused when others can write it or it carries a trigger or a view; consumer JSON that would hash lossily is refused where it enters; implausible provider usage is charged the conservative estimate; a tool schema cannot refer outside itself; every commit is synced. See Security Model.
  • The operator command opens existing databases only, read-only for reading, pages its output with -limit and -offset, sanitises what it prints, requires -approval, -yes, or a typed y before deciding, and binds the decision to the hash of what it showed (ApproveShown, RejectShown). See The Operator Command.
  • The adapters refuse redirects and keys over plain http except to loopback, read only their own environment variables, redact keys, check usage, honour Retry-After, and charge a body cut off mid-read as ambiguous; anthropic.New needs a key and a priced model. See Provider Adapters.
  • The MCP adapter gives a stdio server a minimal environment and its own process group, refuses schemas that refer outside themselves, bounds its connect phase with ConnectTimeout, pins from the raw bytes, and refuses a call that sets a fixed parameter. examples/mcp runs a server pinned by package-lock.json. See MCP Tools.
  • Performance. The loop keeps its steps in memory instead of rereading them every step, so a step's cost no longer grows with the run; synced commits then cost about a third per step on an SSD. See Performance.
  • Project. CI gained a lint job (gofmt, staticcheck, govulncheck, a tidy check) and a release check that builds each nested module against its pinned core; CHANGELOG.md and a written release procedure arrived. See Releasing.

Removed: Store.CreateRun, StatusCreated, and EventRunInterrupted. docs/status.md lists every fix with the test that verifies it, under "Core audit fixes", "Run lease", "Security sweep", and "Performance".

Released: v0.2.1, with the nested modules at v0.1.1

The audit summary for a reply cut off by the output cap or without a tool call now says so instead of reporting an unknown decision kind. Roadmap item 8: CONTRIBUTING.md, SECURITY.md, issue forms, runnable Example functions verified by go test, a pre-1.0 compatibility promise, and every module's go directive at 1.26 with CI on 1.26 and 1.27. No API change; the nested modules re-pin the core and drop to 1.26.

Released: v0.2.0, with providers/ollama, providers/anthropic, providers/openai, and mcp at v0.1.0

Roadmap items 1 and 2. Nested modules carry their own tags with a directory prefix, so go get github.com/joeylking/agent-runtime/[email protected] and go get github.com/joeylking/[email protected] are separate steps. Both consumers deleted the code these modules replace and their recorded replays passed unchanged, which is the byte-faithfulness proof the roadmap asked for. The release also added, from the migrations: matchable sentinels in view and providers, render.DefaultObservation and render.InterruptedText, and ollama.Name and openai.Name.

  • what both consumers rebuilt: the providers helpers, render, trace, view, the NeedApproval and DecodePresentation policy helpers, the cmd/agentrt operator command, the three provider adapters as nested modules, and examples/live. See Provider Adapters, Rendering Model Context, and The Operator Command;
  • the MCP tool adapter: the mcp nested module, which puts an MCP server's tools behind the same validation, policy, limits, and audit log as any other tool. See MCP Tools.

docs/status.md in the repository has a section per item listing every capability with the test that verifies it. Two additions to the core came with them: the deliberately invalid decision kinds truncated and no_tool_call, and the NeedApproval and DecodePresentation helpers on the policy side.

Released: v0.1.2

Fixes two replay recorder defects reported by Casework: provider raw bytes were HTML-escaped and re-indented on disk, and identical requests overwrote each other so replay served the last response for every call.

Released: v0.1.1

Adds operator cancel for a run that is not terminal, found necessary by the first real publication run of Repo Steward: a resume failed after the approval had been granted, and nothing could close the run.

Released: v0.1.0

The runtime that Repo Steward v0.1 runs on. docs/status.md in the repository lists every control with the test that verifies it. In summary, all of the following are verified by named tests:

  • the loop with verbatim decisions, schema validation, policy outcomes, terminal tools, and tool aborts;
  • events committed with state and delivered after commit;
  • hash-bound approvals, approve and resume executing the recorded request, reject, tampering refused, re-pause on a changed policy answer, expiry;
  • interrupted steps with structured reconciliation and no re-execution;
  • the model interface, accounting rows, usage and cost, retries, ambiguous attempts, and call, token, and cost limits with projection;
  • record and replay keyed by canonical request;
  • step, failure, loop, active-time, and elapsed-time limits;
  • a file-backed store surviving close and reopen.

The API is pre-1.0 and changes when its consumer needs it to.

Not yet implemented

  • A blob table for large tool outputs; observations are stored inline.
  • Roadmap items 3, 4, and 6 were adopted by both consumers on 2026-10-03, which met their done-whens; item 7, the write-ups, is the only open item among the first eight.
  • Roadmap items 11 and 12, argument-source rules and policy-gated promotion, are intentions, not promises, in that order, with item 12 as research. Item 10, re-checkable decisions, shipped in v0.5.0. See Roadmap.

Not planned

Multi-agent orchestration, a planning language, prompt templates, memory or retrieval, a multi-provider abstraction, a service mode, and a user interface. The proxy is not a service mode: it is a program a host starts, talks to over stdin and stdout, and opens no listener. Each would be added when a consumer demonstrates the need and not before.

Note what is no longer on that list. An MCP adapter is implemented, because MCP servers turned out to be the clearest case of a large tool surface with nothing enforcing policy in front of it. Provider adapters are implemented as well, and that is not the same thing as a multi-provider abstraction: there are three adapters behind the one two-method interface the runtime already had, with no layer that translates between providers and no runtime type that switches them.

How changes happen

A requirement arrives from a consumer. The abstraction that meets it is added with a test and a status row. Speculative abstractions are refused.

⚠️ **GitHub.com Fallback** ⚠️