Repository navigation
Security Model
SECURITY.md in the repository is the authoritative statement of what the runtime defends against; it is organized by mechanism. This page restates it by who the attacker is, because that is how an evaluator usually asks the question: if the model is hostile, what holds? If the MCP server is? If another account on the machine is? If a dependency is?
For each position: what the runtime does, what it leaves to the operator, and the test or probe that backs the claim where docs/status.md names one. A claim with no test named is one the repository states without a dedicated test; it is marked as such rather than dressed up.
The premise comes from decision 3: repository content, dependency sources, tool output, and an MCP server's tool list are all untrusted input that reaches the model. The model is expected to be wrong, or to be talked into something. What the runtime guarantees is that whatever it concludes, the set of actions it can take is bounded by code it cannot influence.
GitHub's private vulnerability reporting is not enabled on the repository: as SECURITY.md records, gh api repos/joeylking/agent-runtime/private-vulnerability-reporting returned "enabled": false on 2026-09-29. Until it is turned on, do not use the Security tab. Instead:
- Open an ordinary issue saying you have a report to make and asking for a private channel.
- Put no details in the issue, not even in the title.
- The maintainer follows up with somewhere private to send the report.
Never put a vulnerability in a public issue, a pull request, or a discussion. Once private reporting is enabled, prefer it: the Security tab, then Report a vulnerability, which opens a draft advisory only the maintainer can read. Expect an acknowledgement within a week. A fix ships as a patch release of the affected module with an advisory naming the versions.
Only the current release line is fixed: the latest minor of the core, and the latest tag of each nested module. A nested module names the core version it requires, so a core fix may need that module re-tagged too.
The model reads untrusted content and may be steered by it. Assume it asks for the worst thing available.
What the runtime does.
-
Nothing runs without the schema and the policy. Every request is recorded verbatim, validated against the tool's JSON schema, and then evaluated by a policy the agent has no handle to. Malformed arguments become an
invalid_decisionobservation and never reach the policy. (TestDriver_InvalidArgumentsBecomeObservation,TestDriver_PolicyDenyIsObservation,TestDriver_PolicyAbortEndsRun) -
A partial tool call cannot execute. A reply cut off by the output cap or refused carries no tool uses out of the adapter, and
render.Deciderecords such replies as deliberately invalid decision kinds the driver will not act on. (TestDriver_PseudoDecisionKindsAreInvalid) -
An approval binds the exact request. The hash covers kind, capability, presentation, and the request; the recorded request is what executes; policy and the tool's current schema are checked again on resume; only a step's latest approval can be a grant. The model cannot swap the action after a human said yes. (
TestApproval_ApproveThenResumeExecutesRecordedRequest,TestResume_ApprovedArgumentsMeetTheCurrentSchema,TestResume_OnlyTheLatestApprovalOfAStepGrants) -
What the operator reads is what the operator decides.
cmd/agentrt approveandrejectpass the hash of the approval they printed toApproveShownorRejectShown, which compare it inside the deciding transaction, with the update conditional on it. (TestDecide_BindsToTheHashActuallyShown,TestApproval_ShownDecisionIsAtomic) -
Model text cannot rewrite the operator's terminal. Every model-, policy-, or server-chosen field
agentrtprints in text mode passes throughtrace.Sanitize: control characters, bidirectional overrides, zero-width and invisible characters, line and paragraph separators, and the tag block become visible escapes; piles of combining marks are cut. The approval prompt prints one field per line with its byte size, shows a string over 2000 bytes by head and tail, and ends with a lineagentrtwrites itself. A tool result cannot clear the screen, forge an approval block, or push the decision line off screen. (TestSanitize_BidiOverridesEscaped,TestSanitize_ZeroWidthAndInvisibleEscaped,TestShow_BidiAndCombiningMarksInArgumentsEscaped,TestShow_LongArgumentTruncatedHeadAndTail) -
Pathological JSON is refused where it enters. Arguments, results, tool content, capabilities, presentations, and a reconciliation's result nested deeper than 256 levels, or carrying invalid UTF-8, an unpaired escaped surrogate, or a repeated key, are refused as an invalid decision, a tool error keeping the bytes, or a run failed as
internal_error. Nothing stored or hashed is replaced by a stand-in. (TestCheckJSON_RefusesDeepNesting,TestDriver_DeepJSONIsRefusedWhereItEnters,TestCheckJSON_RejectsWhatWouldHashLossily) -
It cannot run forever or spend without bound. Steps, consecutive failures, loops, model calls, tokens, cost, and time are capped by the driver, and token and cost limits stop before the call that would cross them. (
TestDriver_StepLimit,TestDriver_IdenticalCallAndResultStops,TestModel_TokenAndCostLimitsProjected) -
A crash does not let it ask for a side effect again. An interrupted side effect waits for the consumer's reconciliation or an operator. (
TestResume_InterruptedSideEffectWaitsForAnOperator)
What it leaves to the operator.
- What the policy allows. Prompt injection is not detected. If a policy allows a class of action, an injected instruction can use it. The runtime's answer is the bound, not detection.
- What a tool does with its arguments. Schema validation says the arguments fit the schema; path traversal, command construction, and authorization inside a tool are the tool's job. See Writing Tools.
- Secrets in the record. Decisions, arguments, observations, and presentations are stored verbatim. A tool that returns a secret puts it in the database and the audit log; nothing scans or redacts it.
-
An approved re-run. Approving an
interrupted_side_effectruns a call that may already have taken effect. The approval says so; the choice is the operator's.
This covers a model provider's endpoint, anything on the network path to it, and an MCP server, stdio or HTTP. Each is code this project did not write.
What the runtime does with a provider endpoint.
-
Usage it reports is not believed blindly. A negative count, more cached input than input, or a count that is not an integer or does not fit is charged the conservative estimate instead; the reply is not used, not retried, and the run fails as
model_unavailable. Totals and costs saturate rather than wrap negative, so a huge count trips the limits. (TestModel_UnusableUsageIsChargedTheEstimate,TestModel_TotalsSaturateRatherThanWrap,TestUsageCount_OnlyANonNegativeIntegerThatFits,TestGenerate_UnusableUsageIsChargedNothingin each adapter) -
A served but unusable reply is still charged. It comes back as a
ServedErrorcarrying the usage the provider reported. (TestModel_ServedErrorIsChargedAndNotRetried) -
Redirects are not followed, by any adapter or by the MCP HTTP client: a 3xx is a permanent error naming only the host it pointed to, so the key and the prompt stay home. (
TestClient_RefusesRedirects,TestGenerate_RedirectIsRefusedAndTheKeyStaysHome,TestHTTP_RedirectIsNotFollowed) -
A key does not leave the machine over plain http. Over
http://a key goes only to exactlylocalhostor a literal loopback address, and only over a connection whose dialled address is loopback, so a resolver or a proxy cannot carry it off. (This holds for a client whose transport is an*http.Transport; a consumer's ownRoundTripperis left as given.) (TestCheckEndpoint_KeyOverPlainHTTPOnlyToLoopback,TestLoopbackOnly_RefusesAConnectionThatLeavesTheMachine) -
Keys stay out of logs. The OpenAI and Anthropic
ConfigandModelprint, marshal, and log redacted, and an Anthropic SDK error, which holds the request and its key header, is converted before it leaves the adapter. (TestConfigAndModel_EncodeWithoutTheKey) -
Bodies are bounded at 16 MiB. (
TestReadBody_RefusesABodyOverTheLimit)
What the runtime does with an MCP server.
-
It classifies nothing. A tool registers only with an operator rule naming its side-effect class; annotations may refuse a registration and never relax one. (
TestLoad_UnclassifiedToolIsNotRegistered,TestLoad_HintMismatchIsRefusedAndOverridable) -
The pin catches a changed schema or description, hashed from the raw
tools/listbytes with keys matched exactly as the SDK matches them, so a page carrying bothtoolsandToolscannot pin one list and register the other, and a number past 2^53 is not rounded away. (TestLoad_DescriptionChangeRefusesTheTool,TestLoad_SchemaChangeRefusesOnlyThatTool,TestPin_RawListingMatchesKeysExactly,TestMessageTap_SkipsEventsTheSDKIgnores) -
A schema cannot make the runtime read a file or fetch a URL.
mcp.Loadrefuses a schema with an outside$ref,$id, or$schema, and the core compiler would refuse it again. (TestLoad_RefusesASchemaThatReachesOutsideItself,TestNewDriver_SchemaReferencesOutsideItselfAreRefused) -
It cannot hang the client. Connecting, initializing, listing, and Load's checks fall inside
Server.ConnectTimeout; a listing is bounded atMaxToolPagespages andMaxToolstools; an HTTP body is bounded at 16 MiB. (TestLoad_TheChecksFallInsideTheConnectTimeout) -
It cannot override the operator's fixed values, nor set a denied parameter: such a call fails. (
TestDeny_RemovedFromTheSchemaAndRefusedAtCall) -
Its text cannot rewrite the terminal. The load report and Pin's and Load's errors are sanitized. (
TestReport_StringEscapesWhatTheServerChose,TestPin_ErrorEscapesTheServersMessage) -
Results are bounded by
MaxContentBytes, counted on the encoded bytes. (TestCall_OversizedContentIsTruncated) -
A stdio server gets a minimal environment (
PATH,HOME,TMPDIR, plus what the operator names), runs in its own process group on Unix, and is killed with its descendants onClose; on Linux the kernel kills it if this process dies first. (TestStdio_CloseKillsTheServersDescendants; the environment rule has no dedicated row indocs/status.md.)
What it leaves to the operator.
-
What a server does when called. The pin covers what a server presents, not what it does. A server that behaves differently after
Load, or differently from its description, is not caught. Its results are untrusted model input. -
Files and network. A stdio server runs as the operator. The minimal environment withholds variables, not files: credential files under
HOME(~/.aws,~/.ssh, a token cache), anything else the account can read, and any directory onPATHit can write remain reachable, as does the network. Nothing sandboxes it. -
Annotations and output schemas are outside the hashes;
tools/list_changedis ignored until the nextLoad. - A provider that bills differently from what it reports within plausible numbers. Implausible usage is refused; plausible but wrong usage is believed.
Anyone who can write the database can forge an approval, because an approval's hash has no secret in it. This position is about the file.
What the runtime does.
-
The database is owner-only. A new database and its WAL files are created
0600under any umask. A database or WAL file group or others can write is refused withErrInsecureMode; one they can only read is tightened by a writer that owns it; a WAL file owned by another user, or a WAL path that is not a regular file, is refused. (TestStore_FilesAreOwnerOnlyUnderAnyUmask,TestStore_ModesInAChildProcess,TestStore_RefusesADatabaseOthersCanWrite,TestStore_TightensADatabaseOthersCanRead) -
The operator opens what they named.
OpenExisting, whichagentrtuses, refuses a symbolic link withErrSymlinkand anything else that is not a regular file. (TestOpenExisting_RefusesASymbolicLink) -
A crafted database cannot run its author's SQL. A file holding a trigger or a view is refused with
ErrUnsafeSchema, and every connection runs withtrusted_schemaoff and SQLite's defensive mode on. (TestStore_RefusesTriggersAndViewsAndHardensItsConnections) -
A crafted database cannot exhaust the operator's memory.
runs,show, andeventsread only the page they print, with every text column capped atMaxPageTextand totals fromCOUNT. (TestStore_PagesAreBounded,TestRead_OnlyThePageIsReadFromAHugeDatabase) -
Each refusal is one sentence.
agentrtturns an insecure mode, a trigger or view, and a symlink into one sentence at exit 1. (TestOpen_InsecureFileModeRefusedWithOneSentence,TestOpen_TriggerCarryingDatabaseRefusedWithOneSentence,TestOpen_SymlinkRefusedWithOneSentence) -
Recordings are private.
replay.Recordercreates its directory0700and writes each recording0600through a temporary file of its own, synced, then renamed. (TestRecorder_WritesOwnerOnlyThroughItsOwnFile) -
The examples keep their files private.
examples/liveandexamples/mcpkeep their database, sandbox, and manifest underos.UserCacheDir()/agentrt, created0700, andexamples/mcprefuses a manifest or rules file others can write. (TestReadOwned_RefusesAFileOthersCanWrite)
What it leaves to the operator.
-
Root and the owner's own account. Whoever can write
runs.dbcan approve anything in it. The mode checks make another account's write impossible, not the owner's. The main database file's owner is not checked, only its mode. -
Identity. There are no users, roles, or authentication.
-byanddecided_byare labels recorded exactly as given. - Shared storage. SQLite on a network filesystem is unsupported, and so is clock skew between hosts sharing a database; leases and expiry read the local clock. See Run Lease.
- Windows. The mode rules are skipped where file modes do not mean owner, group, and others.
What the project does.
-
Few dependencies, isolated. The core module has exactly two direct dependencies, a JSON Schema validator and SQLite. A provider SDK or the MCP SDK lives in a nested module with its own
go.mod, so a vulnerability in one never reaches the core. -
The example server is pinned.
examples/mcpruns the filesystem server pinned by version and integrity hash inpackage-lock.json, installed withnpm ci --ignore-scriptsand started withnode, instead of whatevernpxfetched that day. (TestServer_RunsThePinnedPackageWithNode) -
CI is locked down. Checkouts set
persist-credentials: false, every job runsgo mod verifybefore building, workflows run with read-only permissions, and every action is pinned to a commit.govulncheckruns in the lint job and the release check. Dependabot holds a version back before proposing it: 3 days for a Go patch, 7 for a minor, 14 for a major, 7 for GitHub Actions. (Implemented;.github/workflows/ci.yml,release-check.yml,dependabot.yml) - Releases are verifiable. A re-pin is never pushed through by bypassing the checksum database, and a pushed tag is never moved. See Releasing.
-
Known advisories are patched. v0.3.0 moved every module past GO-2026-5970 in
golang.org/x/text(the affected symbol was not called) and patchedmodernc.org/sqliteandgolang.org/x/sys.
What it leaves to the operator.
-
The MCP servers you choose. Pinning
examples/mcp's server is the example's choice; a consumer pins its own. -
Memory safety of the SQLite driver.
modernc.org/sqliteis C SQLite transpiled to Go, outside the memory-safety guarantees the rest of the program has, as a cgo binding to the original C would be. - The process. The agent, tools, policy, and runtime share one address space. There is no sandbox: a tool can do whatever the process can, and a tool's side-effect class is a claim the operator makes, not something the runtime verifies.
Safety Properties is the same ground stated as guarantees. Run Lease and Persistence and Inspection are the mechanisms behind the third position.
Using it
- Getting Started
- Upgrading to v0.5
- Upgrading to v0.4
- Upgrading to v0.3
- Concepts
- Writing an Agent
- The Gate
- Writing Tools
- MCP Tools
- MCP Proxy
- Writing a Policy
- Models and Replay
- Provider Adapters
- Rendering Model Context
- Approvals and Resume
- Interruption and Reconciliation
- Run Lease
- Limits
- Testing a Consumer
- Re-checking a Run
- Measuring Agents
Inside it
- Internals
- Persistence and Inspection
- Performance
- The Operator Command
- Approval Channels
- Exporting Events
- Testing
Deciding on it
- Safety Properties
- Security Model
- Comparison with Frameworks
- Status and Roadmap
- Roadmap
- Design Decisions
- Essays
- Contributing
- Releasing
Reference