Status and Roadmap - joeylking/agent-runtime GitHub Wiki
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.
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 intool_specs; a policy implementingIdentifiedPolicyhas its identity recorded on each step it evaluated; andstep.policycarries the spec the policy was handed and aviewof 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, andEventHashread it, andagentrt 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.Headgives one to keep. -
agentrt.Recheckreplays each recorded policy evaluation through a policy, rebuilt as the policy saw it, and reportssame,different,not_a_policy_evaluation, ornot_recheckable, withSame,Rechecked, andComplete.trace.WriteRecheckrenders 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, andScenario.Recheckdo 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.2carriesagentrt.policy.idandagentrt.policy.spec_hash. -
examples/recheckshows 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
PolicyJSONform 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.
| 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 |
| 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 |
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, andVerdictgive the runtime's controls to a loop the runtime does not own, and theDriverruns on the same code. It also broughtDecision.Origin,Session.Input, aGrantTTLre-check atExecute, andtestkit.Scenario.Loopwith 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-proxyis 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.CanonicalJSONreturns the form the runtime hashes. It is the existing encoder, exported, and every hash is unchanged (TestContentHash_Golden). -
mcp:ReadOwned,OwnedPrivately,ReadManifest,ReadRules, andFileRule, the operator-file readersexamples/mcphas, added to the module so the proxy reads the same files under the same rule.examples/mcpis 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.GrantTTLagain atExecute. This was never in a release, because the gate is new, and theDriveris unaffected.
| 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 |
| 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.
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.
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
benchmodule (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, andComparerefuses 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
testkitpackage (item 4).testkit.Runcrashes 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
exportpackage and theexport/otelmodule (item 5).export.Followerdelivers a database's events in commit order from a cursor, read-only and from a separate process, with a JSON Lines sink.export/otelturns them into OpenTelemetry spans with ids derived from the run's own, andexport/otel/examplesends a database's runs to the collector theOTEL_*environment names. Checked against a stock collector on 2026-10-02. See Exporting Events. -
Approving from a chat bot or a browser: the
approverandapprover/webhookpackages (item 6).approver.Approvershows an approval, approves, rejects, or cancels the run, andapprover/webhookserves it over HTTP behind an HMAC signature, a timestamp window, replay refusal, a rate limit, and a body bound.examples/approverruns 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, theOperatortype, andCompileTool,ToolSchema.Check, andCheckArgs. See Persistence and Inspection, Approvals and Resume, and Writing Tools. -
Fixes.
approve </dev/nullnow 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.cancelon a finished run names the run's status instead of reporting an approval already decided, becauseviewnow matches the core's sentinel errors witherrors.Isinstead of searching their messages.show's nested JSON indents consistently, andagentrt's usage text lists-limitand-offset. See The Operator Command. -
Documentation corrections. The README and changelog now name the third case where v0.3.0 changed what
renderproduces: 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 adocs/status.mdrow 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".
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
ResumegetsErrRunLeased; 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_effectapproval. 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
-limitand-offset, sanitises what it prints, requires-approval,-yes, or a typedybefore 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.Newneeds 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/mcpruns a server pinned bypackage-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.mdand 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".
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.
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
providershelpers,render,trace,view, theNeedApprovalandDecodePresentationpolicy helpers, thecmd/agentrtoperator command, the three provider adapters as nested modules, andexamples/live. See Provider Adapters, Rendering Model Context, and The Operator Command; -
the MCP tool adapter: the
mcpnested 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.
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.
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.
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.
- 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.
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.
A requirement arrives from a consumer. The abstraction that meets it is added with a test and a status row. Speculative abstractions are refused.