AgentLock™
An adversarially benchmarked reference implementation for pre-action agent authorization.
Your AI agent needs a login screen. AgentLock is that login screen. Secure tool calls with declarative, framework-agnostic authorization blocks.
or pip install agentlock[crypto] for Ed25519 signed receipts
AgentDojo, gpt-4o-mini, tool_knowledge attack, single runs at the benchmark's default sampling. See the whole table, including where the gate costs more than it saves.
The Authorization Gap
Unix, databases and cloud APIs each have a formal permissions layer. Most AI agent tool calls have none: if an LLM generates a tool call, the tool executes. This is the "Full Permission" anti-pattern.
Prompt Injection Risk
Malicious input can trick agents into calling tools they shouldn't.
Data Exfiltration
Unrestricted tool access leads to unauthorized mass data reading.
| System Layer | Auth Mechanism | State |
|---|---|---|
| Unix/Linux | User/Group (rwx) | SECURE |
| Databases | GRANT/REVOKE (CRUD) | SECURE |
| Cloud API | IAM / OAuth Scopes | SECURE |
| AI Agent Tools | None (Plain JSON) | AT RISK |
Interactive Simulation
Test the AgentLock gate yourself. Configure the context, attempt tool calls, and explore a browser simulation of the gate's decision pipeline.
Tool Permissions Block
Try it: execute send_email (allows), click Fetch web page, execute the identical call again (denies). Nothing about the call changed. Only the session's provenance did.
Gate Pipeline
Gate Response JSON
Session Audit Log (simulated)
This is a UI simulation of AgentLock's decision logic, including the provenance-lineage gate. The real engine is pip install agentlock; the quickstart below runs the same allow-then-deny flip against the real engine.
{
"name": "send_email",
"description": "Send an email to a recipient",
"parameters": {
"to": { "type": "string" },
"subject": { "type": "string" },
"body": { "type": "string" }
},
"agentlock": {
"version": "1.5",
"risk_level": "high",
"requires_auth": true,
"allowed_roles": ["admin", "customer"],
"scope": {
"data_boundary": "authenticated_user_only",
"max_records": null,
"allowed_recipients": "known_contacts_only",
"recipient_parameter": "to"
},
"rate_limit": {
"max_calls": 5,
"window_seconds": 3600
},
"data_policy": {
"output_classification": "may_contain_pii",
"prohibited_in_output": ["ssn", "credit_card"],
"redaction": "auto"
},
"context_policy": {
"source_authorities": [
{ "source": "system_prompt", "authority": "authoritative" },
{ "source": "user_message", "authority": "authoritative" },
{ "source": "web_content", "authority": "untrusted" }
],
"reject_unattributed": true
},
"audit": { "log_level": "full" },
"human_approval": { "required": false }
}
}The AgentLock Block
A declarative metadata block that travels with the tool definition. Any agent framework can enforce security before a tool call hits your backend. No vendor lock-in required.
- 1
Declarative Security
Stop hardcoding permission checks. Declare them in the tool schema: roles, scope, data boundaries, and redaction rules.
- 2
Single-Use Tokens
Tokens are single-use and bound to the exact parameters authorized; a replayed token is rejected (TokenReplayedError).
- 3
Audit Ready
Each authorize() decision, allow or deny, writes a structured audit record through a pluggable backend protocol (file, in-memory and async backends ship; a SIEM backend is yours to plug in).
The AgentLock Architecture
Layer 1: Agent
Conversation & Decision. Generates tool call intent.
Layer 2: Gate
The AgentLock Gate. Intercepts intent, validates identity and permissions.
Layer 3: Tool
Execution. Only runs if Layer 2 issues a one-time token.
Why this matters:
By separating the intent to call from the permission to call, AgentLock prevents autonomous agents from making dangerous mistakes or being manipulated by adversarial prompts. It brings Zero Trust to the AI tool ecosystem.
What AgentLock Prevents
AgentLock is designed to stop the most critical attack categories against AI agents.
| Attack Category | How AgentLock Prevents It |
|---|---|
Prompt Injection | Permission blocks are enforced at the infrastructure level, not by the LLM. Even if the agent is tricked, the gate denies unauthorized calls. |
Social Engineering | Role-based access control prevents agents from performing actions outside their assigned role, regardless of conversational manipulation. |
Data Exfiltration | Data boundary enforcement (authenticated_user_only, team, organization) and max_records limits restrict what data an agent can access. |
Privilege Escalation | Allowed roles are declared per-tool and validated by the gate. An agent cannot grant itself higher permissions. |
Tool Abuse | Rate limiting with sliding window enforcement prevents runaway loops, brute-force attacks, and excessive API consumption. |
Token Replay | Every execution token is single-use, operation-bound (SHA-256 of parameters), and time-limited. Replay attempts raise TokenReplayedError. |
Agent Impersonation | When a session exists for a user_id, its role is authoritative: a differing claimed role is denied with role_mismatch (1.10.0). As of 1.11.0 a call can address its session by session_id, and an id that is missing, expired or held by another user is denied with session_not_found rather than substituted. With no session, the gate trusts the host to have authenticated the caller. |
Memory Poisoning | Data policy enforcement with output classification and automatic redaction prevents sensitive data from leaking into agent memory. |
Indirect Prompt Injection (write-trailing-read) | Provenance-lineage gate: untrusted reads gate subsequent consequential writes. Deferred commit re-checks at end of turn, and as of 1.11.0 also re-checks registration, role, session liveness and policy version before committing. |
Indirect Prompt Injection (multi-hop laundering) | Cross-hop provenance linking: a value relayed through an intermediate tool and then used in a consequential call is traced to its untrusted origin and denied, citing the relay entry. Enforcement is bounded by carriage, not hop count. |
Recipient Redirection | Recipient policy is enforced from the trusted permission block, with the recipient read from the tool's declared parameter. A send to an address outside known contacts, the allowlist, or the user's domain is denied before execution, and an asserted recipient that disagrees with the parameter is denied as laundering. |
What's New in v1.11
External review of the 1.10.3 wheel. The reviewer reported four enforcement gaps, and each was reproduced against the 1.10.3 commit before any fix was written. A fifth, a fail-open in SessionStore.destroy, was found while fixing the second. tests/test_v111_review_gaps.py holds the acceptance tests.
Built by measurement: each fix was built as candidates behind acceptance tests, and one candidate, a full re-authorization at deferred commit, measured worse and was removed. Baseline, decisions and predictions
Security
Deferred Commit Re-Checked (G1)
Through 1.10.3 a queued action was re-decided at commit on taint alone, and an untainted record committed without the registry being consulted: measured at the baseline, records committed after their tool was unregistered, after the caller's role was removed from allowed_roles, and after the caller's session was destroyed. Every record is now re-checked first, tainted or not, each failure with its own reason: unregistered_at_commit, and the new role_revoked_at_commit, session_gone_at_commit and policy_changed_at_commit, the last comparing the permission block against a fingerprint taken at queue time. A policy change that only widens the block still denies the queued record; re-queue it under the new policy.
Session Addressing (G2)
authorize() and execute() now take session_id=. Through 1.10.3 the session was resolved by user_id, which returns the user's newest live session, so with two live sessions for one user the older session's taint was invisible and the newer session's taint governed calls meant for the older one. An addressed session is resolved by id and nothing else: a missing or expired id, or one held by another user, is denied with the new reason session_not_found and is never substituted, and in execute() that refusal comes before the token is consumed. Without session_id the user lookup runs as before, and the audit record says so with session_resolution "by_user".
One Live Session per User (opt-in)
AuthorizationGate(single_session_per_user=True) makes create_session raise SessionConflictError for a user who already holds a live session, unless the caller passes replace=True, which destroys the live session first and audits that as session_replaced. It is off by default in 1.11.0, and with it off a second session supersedes the first silently, as through 1.10.3, for every call that does not address a session by session_id. It is recommended for every deployment that does not deliberately run concurrent sessions per user, and it becomes the default in 2.0.
No Orphaned Sessions
SessionStore.destroy(session_id) cleared the user's session pointer whether or not it still pointed at the destroyed session. With sessions A then B for one user, destroying A left B live but unreachable by user lookup, so a user-addressed call found no session and was allowed as host-trusted: measured, a call denied untrusted_lineage on tainted B became an allow once A was destroyed. The pointer is now cleared only when it points at the session being destroyed. Present through 1.10.3, and found while fixing G2.
HTTP Adapters Authorize the Body (G3)
Through 1.10.3 neither the FastAPI nor the Flask adapter passed parameters to authorize(), so every request was authorized as the empty call: measured at the baseline, a call carrying an attacker URL that the gate denies on param_lineage when given its parameters was allowed with none. AgentLockMiddleware, require_agentlock, agentlock_required and AgentLockFlask now pass the JSON body as the parameters and fail closed on anything else. An empty body is the empty call {}; an application/json body that does not parse to a JSON object is refused with 403 and reason invalid_json_body; a non-empty body of any other content type is refused with 403 and reason unsupported_content_type, and the handler does not run. The FastAPI middleware replays the same bytes to the handler, so the handler reads what was authorized. The adapters still do not call execute().
Fixed
Structured Results Redacted (G4)
Redaction under prohibited_in_output ran only on str results, so a tool returning a dict or list handed its prohibited data back untouched. It now walks every str leaf of dict values and list, tuple, set and frozenset items, recursively, on every path that applies a data policy: gate.execute(), and so gate.call() and the sync @agentlock wrapper, and the async @agentlock wrapper, which now goes through the new public gate.redact_result(). Dictionary keys are not redacted, container types are rebuilt, and a clean result is returned as the original object, not a copy.
Decorator Sessions and Type Checking
Both @agentlock wrappers popped _session_id and discarded it, so every decorated call read the user's newest session. The sync wrapper now passes it as session_id= to gate.call(), which forwards it to authorize() and execute(); the async wrapper passes it to authorize() and to its audit calls. DeferralManager.resolve_commit_queue now type-checks, so mypy agentlock/ reports 0 errors, with no behavior change.
Measured
1.11.0: 1912 passed, 9 skipped, 1 xfailed with the crypto and mcp extras plus fastapi, flask and python-jose. The one expected failure is a strict xfail recording G2 probe row 1, which stays open by design under the defaults (see Known limits). Every gap was first reproduced at the 1.10.3 commit against predictions frozen before the first run, and every prediction matched.
Known limits
- bytes tool results are not redacted. The structured-result walk returns a bytes result, or a bytes leaf, untouched.
- The HTTP adapters do not authorize the query string. Only the JSON body is passed to authorize(); query parameters reach the handler without being seen by the gate. A route that reads tool parameters from the query string is authorized as the empty call for those parameters, as every route was through 1.10.3. Form bodies are not a gap of the same kind: a non-JSON body is refused before the handler runs.
- None of the files under agentlock/integrations/ passes session_id to authorize(). The @agentlock decorator does, as of this release, via _session_id.
- single_session_per_user is off until 2.0. With the 1.11.0 defaults, a second create_session for a user who holds a tainted session succeeds, and a later call that addresses neither session reads the clean new one and is allowed. Pass session_id= to authorize() and execute(), or set single_session_per_user=True, to close it. The strict xfail in tests/test_v111_review_gaps.py records this row.
- A redacted set can shrink. Redacted set members can collide, so a rebuilt set or frozenset may be smaller than the original.
What's New in v1.10
Integration hardening. An external review of the published 1.9.1 wheel found seven groups of issue and supplied a runnable 33-test oracle, committed verbatim as the definition of done. Six of the seven were places where the engine's own execution paths did not carry a decision the gate had already made. 1.10.1 and 1.10.2 close what that reviewer's rechecks found, and 1.10.3 adds an opt-in escalation for derived-origin external actions.
Not additive
1.10.0, 1.10.1 and 1.10.2 are not additive. Several fixes change behavior on paths that were failing open, and code that relied on those paths will be denied or transformed where it previously was not. Two shapes of caller are affected directly: a caller that supplied a role for a principal who also holds an authenticated session is now denied with role_mismatch unless the claim and the session agree, and as of 1.10.2 a bearer token carries no identity at all unless jwt_key is configured. Read the changelog before upgrading.
One Execution Contract
A declared transformation now reaches the tool and the caller on each in-repo execution path: gate.execute(), gate.call(), both decorators, both MCP hooks and the AutoGen map. The HTTP adapters do not call execute() (see below). Before 1.10.0, a tool declaring redact_pii on its output returned the raw SSN to five of six callers. The output walk covers str, bytes, dict, list, tuple, set and frozenset, and both payloads of an MCP result. The execution token is bound to the effective parameters, not the requested ones.
Authoritative Identity
The authenticated session's role is authoritative over the caller's claim: a differing claimed role is denied with the new reason role_mismatch before any policy step runs. MCP identity is resolved host first: for a field the host configured a default for, the client's value is stripped and ignored, and the substitution is audited as identity_override_ignored. HTTP tool selection is decided by the server's route mapping, and a conflicting X-AgentLock-Tool header is refused with 403.
Verified Bearer Tokens (1.10.2)
1.10.1 and earlier accepted unverified bearer token claims as identity in both HTTP adapters. A bearer token now carries no identity unless jwt_key is configured. With a key set, a token that fails verification is 401 (jwt_invalid), a request with no bearer credential is 401 (jwt_required), the identity headers are never consulted, and "none" is refused as an algorithm. With no key, the X-AgentLock-* headers are trusted-upstream inputs, and a deployment exposed to untrusted clients has to strip them at its edge.
Deferrals That Stay Decided
Deferred commit re-checks parameter and novel lineage against the completed context. Parameters are snapshotted at queue time, a terminal resolution stays terminal, expiry is enforced where the queue is resolved, and a failed async call no longer leaves its token ACTIVE. As of 1.10.2, an execution reported after a TIMEOUT denial is flagged as one rather than logged as a routine completion.
Paths and Recipients (1.10.1)
whitelist_path resolves with filesystem semantics before it normalizes anything, so a symlink followed by .. can no longer escape the prefix, and on allow it returns the resolved path it checked. restrict_domain splits the value on comma and semicolon and checks each address in it, not only the first, and an at sign it cannot parse blocks the value. One MCP payload walker now serves both the declared transformation and the data policy.
Derived-Origin Escalation (1.10.3, opt-in)
New LineagePolicyConfig.gate_derived_origin, off by default. An external action driven by tool output or agent memory escalates to needs_approval with the new denial reason derived_origin_lineage. It does not read content and cannot tell a benign act from an attack: measured on seven frozen cells, it catches 4 of 4 attacks and puts a human in the loop on 3 of 3 legitimate cases, which is the disclosed price. The same release strips a trailing period from str lineage tokens, which removes false-positive denials and adds none.
Measured
1.10.3: 1834 passed, 9 skipped, 0 failed on CPython 3.14.6 with the crypto and mcp extras plus fastapi, flask and python-jose. The external reviewer's two oracle files, 65 cases and 31 cases, are committed verbatim and are the definition of done.
What it does not do
- Dictionary keys are not modified. The output walk descends into values only. A key is a field name, and a transformation that renamed fields would corrupt the payload it was asked to sanitize. A tool that puts a secret in a key has to redact it itself.
- Arbitrary objects are returned as they came. The walk covers str, bytes, dict, list, tuple, set and frozenset, and rebuilds each of those. Anything else is handed back untouched, whatever its __str__ says. Bytes that are not valid UTF-8 are decoded lossily before the transformation sees them, so the readable part is transformed and the unreadable part is neither transformed nor preserved.
- The __wrapped__ boundary is where the gate's guarantee ends. A wrapper that advertises one signature and alters the arguments before calling the inner function is the application's own code, on the trusted side of the boundary, and is outside what the gate can check.
- The HTTP adapters pass no body parameters, so the per-parameter checks do not run on an HTTP request. They authorize the identity, the role, the session, the rate limit and the tool's own permission block. They do not reach scope limits, parameter lineage, novel lineage, a declared parameter transformation or the output modifier, and neither adapter calls execute(). A deployment that needs the execution contract on an HTTP route has to apply it inside the handler. As of 1.11.0 both adapters authorize the request's JSON body as the tool parameters and refuse any non-empty body of another content type; the query string is still not authorized, and neither adapter calls execute().
- whitelist_path is canonicalization at authorization time and not a race resistant filesystem sandbox. Between the gate's look and the host's open() a component can still be replaced. A host that must be safe against an actively hostile filesystem has to open the file safely itself.
- restrict_domain: a value carrying no at sign anywhere is returned unchanged, because an allowlist over domains can only govern things that have a domain. A display name containing a comma, as in "Doe, Bob" <bob@company.test>, is blocked, and a bare local name with no domain is not covered.
- Two MCP payloads are not descended into. A ResourceLink is passed through whole, both its uri and its name, and a result's _meta is not walked. BlobResourceContents is passed through unchanged. A host serving sensitive material in any of these has to redact it at the source.
- A verified token that carries no exp claim is accepted, and it never expires. Token lifetime is the issuer's decision. A deployment that needs every token to expire has to make its issuer say so; nothing on this side will notice a token that does not.
- The derived-origin verdict is needs_approval, which is not gate-level unattended deny. It does not reach StepUpManager, so there is no request id, no notifier and no timeout-to-deny. What an approval means, prompt a human or refuse outright, is the deployment's behaviour, not the gate's.
- A derived_origin_lineage denial carries no lineage_evidence block. The denial carries its reason and its detail. Richer evidence for derived-origin denials is a follow-up.
- The derived-origin escalation applies at authorize() time only. A write deferred and later committed is not re-gated on derived origin.
- session_write_gate=False makes the derived-origin branch inert, and it records no shadow of its own: no session_gate_shadow is written for what the branch would have blocked.
- The standalone adapters carry none of the 1.10.0 execution contract. None of them applies a declared parameter or output transformation, and mcp-agentlock resolves identity client-first exactly as the in-repo hook did before 1.10.0.
What's New in v1.9
Enforcement completeness. An external review of the published 1.8.0 wheel found three places where the engine did not enforce what its own documentation said it enforced. 1.9.0 fixes all three, and 1.9.1 closes four more binding gaps found by the same review and a pre-release red pass. Neither release adds a detection feature, a denial reason or a schema change. 1.9.0 is not additive: two of its fixes change behavior on paths that were failing open, and code that relied on those paths will now be denied.
All Arguments Reach the Gate
The decorator wrappers and the AutoGen map built the gate's parameters out of kwargs alone. A tool declaring recipient_parameter="to" denied send(to=hostile) and executed send(hostile). Calls are now bound to the function's signature with defaults applied before authorization, and the function is invoked from that same binding. This covers the engine's own decorators and in-repo integrations; the standalone adapters are listed below.
Tokens Bind the Empty Call
A token obtained by authorizing nothing used to execute anything. The parameter hash is now always computed, the empty call included, and the comparison is unconditional. Caller contract: the parameters passed to execute() must be the parameters passed to authorize(). None and {} are the same call, and a mismatch raises TokenInvalidError.
MCP Fails Closed, on Both SDKs
Under mcp 2.x the wrapper used to install nothing, and every tool handler ran ungated. A server the wrapper cannot hook now raises IntegrationUnsupportedError at construction. AgentLockMCPServer installs on both SDK generations: it patches @server.call_tool() under 1.x and wraps add_request_handler under 2.x, and both majors are tested against the real SDK.
Binding Rules
A callable whose signature cannot be read refuses to be wrapped (1.9.0). In 1.9.1: a **kwargs key that names another parameter is refused instead of flattened over it, a functools.partial is bound through to the function underneath, a recipient is read for the characters it holds so a str subclass cannot name a known contact to the gate and an attacker to the application, a declared recipient parameter the signature can never carry is refused at wrap time, and a recipient hidden in *args is refused at call time. Each raises BindingError.
Measured
1.9.0: 1503 passed, 9 skipped with mcp 2.2.0. 1.9.1: 1520 passed, 9 skipped with mcp 2.2.0. 0 failed in every environment, and mypy reports 0 errors, down from 4. Each 1.9.0 gap was first committed as a strict xfail measured failing against the engine, and the markers came off as the gaps closed.
What it does not do
- The engine's own decorators and in-repo integrations bind every call argument, and that is the whole of what these releases change. The standalone adapters ship from their own repositories and are updated separately. At their current releases, crewai-agentlock 0.2.0 and langchain-agentlock 0.1.0 authorize keyword arguments only, and of those two only crewai-agentlock carries positional arguments past the gate into the wrapped call; mcp-agentlock 0.2.1, openai-agentlock 0.1.0 and openclaw-agentlock 0.1.0 hand the gate the same mapping they hand the tool and have no positional route.
- No standalone adapter applies the wrapped function's defaults, so a parameter the caller omits and the function defaults is not seen by the gate in any of them. None of them binds through partials, coerces recipient strings, or checks that a declared recipient parameter is observable.
- Threat model: the gate binds to the callable's signature as inspect reports it, following __wrapped__. A wrapper that advertises one signature and alters the arguments before calling the inner function is the application's own code, on the trusted side of the boundary, and is outside what the gate can check.
- A deployment calling a function whose signature genuinely has both a parameter and a **kwargs key of the same name will see BindingError where it previously saw execution.
What's New in v1.8
Recipient policy enforcement. allowed_recipients has been in the schema since 1.0 and was never enforced: the denial reason existed with no raise site and Step 8 of the policy pipeline was a comment. v1.8 implements it, opt-in by schema version, with the recipient read from the tool's own parameters so no adapter change is needed.
Four Policies, Defined and Tested
known_contacts_only checks the recipient against contacts supplied by the deployer at session creation, never from tool or model output. allowlist checks exact addresses or domain entries, exact domain only, no subdomains. same_domain requires the recipient's domain to equal the session user's domain. any leaves the step off. Malformed recipients deny under every restrictive policy.
The Block Declares the Parameter
recipient_parameter names which top-level tool argument carries the recipient. The gate reads that one key itself. A caller-asserted recipient that disagrees with the declared parameter is denied, because a benign assertion over a hostile parameter is the laundering shape. List-valued parameters are checked as a set; the first failure wins.
Additive by Version
Enforcement is live only for blocks that declare version 1.5 or later. Blocks at 1.4 and below decide exactly as they did in 1.7.0. This is the same opt-in mechanism 1.3 used for lineage. Schema 1.5 is published; a 1.4 block still validates against it, measured.
Measured
1495 passed, 8 skipped with the [crypto,mcp] extras, 0 failed. Verified end to end through the in-repo MCP integration against the real mcp SDK: the recipient reaches Step 8 and the tool never executes on a denial. Every recipient denial carries a signed receipt when a signer is configured. The pre-registered predictions, including three prediction wording defects found at measurement time and recorded before the affected commits, ship in the repo.
gate = AuthorizationGate()
gate.register_tool("send_email", {
"version": "1.5",
"risk_level": "high",
"requires_auth": True,
"allowed_roles": ["support"],
"scope": {
"allowed_recipients": "known_contacts_only",
"recipient_parameter": "to"
}
})
gate.create_session(user_id="alice", role="support",
known_contacts=["bob@company.com"])
gate.authorize("send_email", user_id="alice", role="support",
parameters={"to": "bob@company.com"}) # allowed
gate.authorize("send_email", user_id="alice", role="support",
parameters={"to": "attacker@evil.com"}) # denied: recipient_not_allowedWhat it does not do
The FastAPI and Flask integrations authorize on tool name from headers and pass no parameters, so recipient checks are unreachable through them; that is pre-existing and now stated. (As of 1.11.0 both adapters pass the JSON body as the parameters, so recipient checks reach body parameters; query-string parameters are still not authorized.) The standalone adapters (mcp-agentlock, crewai-agentlock) reach recipient enforcement once a deployer declares recipient_parameter and agentlock 1.8 or later is installed, but their dependency floors have not been raised to 1.8: as of October 29, 2026, mcp-agentlock 0.2.1 and crewai-agentlock 0.2.0 still declare agentlock>=1.7, so an install that resolves to 1.7 has no recipient enforcement. A rate-limit denial still returns without a receipt; that is recorded as an open item.
What's New in v1.7
Cross-hop provenance linking. A value that arrives from an untrusted source, passes through an intermediate tool, and then reaches a consequential call is still traced back to where it came from.
Cross-Hop Linking
A derived entry whose ingestion carries a prior entry's whole content records that entry as its parent. The link is established by containment at write time, when the derived entry is recorded, not reconstructed later from the tool call.
Taint-Reachability Walk
Decision-time checks follow the recorded links via a cycle-guarded walk. A value relayed through a derived tool and then used in a consequential call is denied with reason param_lineage, citing the relay entry as the untrusted origin, not only the direct source the parameter came from.
Live in the Adapters
Cross-hop enforcement runs end to end in the crewai and mcp adapters against the published engine. The suite is 1418 passed and 7 skipped with the [crypto,mcp] extras installed.
What it does not do
Linking requires whole-content carriage: the derived entry must carry the prior entry's content in full, meeting the containment floor. A value that is rewritten, truncated below that floor, or never carried into the downstream call does not link, and no parent is recorded for it. Encoded-payload behavior is measured at the engine level, not yet through an adapter corpus.
What's New in v1.6
Derivation taint. Values that arrived from an untrusted source and then appear in a tool-call parameter in an encoded or reformatted shape are still traced back to that source, without ever decoding the parameter.
Value-Identity Normalization (Family 1)
The same value written a different way is now the same value to the gate. Canonical forms for dates, phone numbers, amounts, and defanged URLs are emitted on both sides of the comparison, additively: a canonical form is added alongside the raw token, never replacing it, so no existing match can be removed by adding one.
Forward-Encode, Never Reverse-Decode (Family 2)
The engine takes the untrusted tokens it already recorded, applies base64, hex, and natural-URL encoding to each, and matches parameters against those forms. No parameter value is ever decoded. A shipped test greps the engine's context module for decode primitives and fails if any appear. Both bare encoded values and encoded values embedded inside longer tokens are covered.
The frozen claim
"In a deployment that registers the tool at permissions version 1.3 or later with param_lineage_enabled set, and declares the untrusted context source on the writes it records, a tool-call parameter carrying a bare or composite encoded form of a url-kind or email-kind untrusted value, under base64, hex, or natural-URL encoding, is attributed back to its parent untrusted provenance entry, without decoding any parameter value, with novelty gating off."
What it does not do
Encoded attribution is verified at the engine level. It does not decode parameters, so an encoded value whose plaintext never entered the session as untrusted content has no form to match against, and a semantically rewritten value is out of reach. Encodings outside base64, hex, and natural-URL, and nested encodings, are not covered.
What's New in v1.5
The evidence layer: grant basis recording, execution confirmation, provenance on denials, and deferred-resolution logging. v1.5 records strictly more and decides identically, verified by A/B replay with zero decision diffs across 4542 replayable decisions.
Grant Basis Recording
An allowed audit record now carries a grant_basis: which lineage checks evaluated, what they concluded, and which never ran and why. It separates "ran and nothing matched" from "had nothing to compare". Across 3261 replayed grants, only 4.0% (129) support the claim that the arguments were checked against untrusted content and came back clean.
Execution Confirmation
The gate captures whether a permitted call actually executed, closing the loop between authorization and action. A decision that was allowed but never ran is distinguishable in the record from one that allowed and executed.
Provenance on Denials
Denials now carry the provenance that produced them: which untrusted origin, which lineage taint, which deferred re-decision flipped the outcome. The reason a call was blocked is captured with the block itself.
Deferred-Resolution Logging
When a deferred action is re-decided at end of turn, the full resolution is logged: the initial state, the content that arrived after the call, and the final decision. Nothing about a deferred outcome is left implicit.
Integrations Extracted from Core
The LangChain and CrewAI integrations moved out of core into standalone packages, langchain-agentlock and crewai-agentlock. Core stays lean and framework-agnostic; adapters version on their own cadence.
v1.5 is an evidence and architecture release. Decision behavior is unchanged: the A/B replay confirmed zero decision diffs across 4542 replayable decisions.
What's New in v1.4
Selective action-class gating, novel lineage, action-class audit, needs_approval surfaced at the gate boundary, and schema v1.4.
Selective Action-Class Gating
Declare which action classes each tool belongs to. Deletions and membership changes stay taint-gated because no parameter value betrays them. Value-carrying writes release to parameter lineage, which covers them. The one declaration that weakens gating can only come from the tool's trusted registration block, never a caller.
Benchmarked, Pre-Registered
On AgentDojo's travel suite (gpt-4o-mini, tool_knowledge attack), selective action-class gating raised utility from 30.00% to 51.43%, which is 79% of the benign ceiling of 65.00%, while defense-effective attack success went from 2.14% to 0.00%. The defended agent outperformed the undefended agent under attack (36.43%). Predictions were pre-registered and committed before any run launched. On slack, no utility was recovered, by design: the suite has no soundly declarable value-carrying tool, and the control run confirmed the gate refuses to relax where relaxing is unsound.
Full report and pre-registrationAction-Class Audit
Run audit_action_classes() to see each tool with an enabled lineage policy as DECLARED, UNDECLARED, or NOT_COVERED, backed by what your traffic actually asserted. It will never confidently suggest the one declaration that weakens gating; that one requires a human.
Novel Lineage
Per-target classification of trusted, untrusted, and never-before-seen values, checked above the coarse taint gate.
v1.3: Provenance-Lineage Gating & Deferred Commit
Provenance-lineage gating, parameter lineage, deferred commit, and an external AgentDojo evaluation.
Provenance-Lineage Gating
Gates consequential writes on the provenance of what is already in the session. After an untrusted read (web content, external messages), consequential writes are blocked. The gate never inspects tool-call content, so there is nothing for an attacker to phrase around.
Parameter Lineage
Opt-in per tool (param_lineage_enabled, off by default): each parameter of a call to that tool is checked against values that originated in untrusted context. An attacker-planted URL or email is denied even when the tool call itself looks legitimate. The same value supplied in the user's own authoritative request is allowed.
Deferred Commit
Consequential actions are queued and re-decided at end of turn against the complete session provenance. Content that arrives after the call can still deny it.
Evaluated on AgentDojo
On the write-trailing-read threat model, the provenance-lineage gate achieved 0% defense-effective attack success on the banking and workspace suites across two models (GPT-4o-mini and GPT-4o), at a measured utility cost on benign tasks.
Read the paperAll v1.3 features are backward compatible and inert unless a lineage_policy is enabled.
v1.2: Adaptive Hardening & Decision Types
Adaptive hardening, three new decision types, and multi-signal threat detection.
Adaptive Prompt Hardening
Pre-LLM threat detection scans user messages before the model processes them. Dynamic system prompt injection based on real-time session risk scoring.
MODIFY Decision Type
Transform tool outputs before the LLM sees them. PII redaction, domain restriction, path whitelisting. The tool runs but sensitive data never enters the model context.
DEFER Decision Type
Suspend ambiguous tool calls when context is insufficient. Auto-denies on timeout. Catches first-turn attacks on high-risk tools.
STEP_UP Decision Type
Require human approval when session risk is elevated. Catches multi-tool escalation patterns and post-denial retries.
5 Decision Types
ALLOW, DENY, DEFER, STEP_UP, MODIFY. Beyond binary allow/deny.
4 Signal Detectors
Behavioral velocity, tool combination anomaly, response echo detection, and pre-LLM prompt scanning.
Ed25519 Signed Receipts (AARM R5)
With a ReceiptSigner configured, an authorization decision produces a cryptographically signed receipt, verifiable offline without gate access. Rate-limit denials return before that path and carry no receipt. Ed25519 default with HMAC-SHA256 fallback. Install with pip install agentlock[crypto].
Hash-Chained Context (AARM R2)
Each recorded context entry includes the hash of the previous entry, forming a tamper-evident append-only chain. verify_chain() recomputes the hashes and reports the index of the first entry that no longer matches.
First-Call Deferral
Defer the first tool call in any session regardless of risk level. Catches first-turn attacks before signals accumulate.
Deny-on-Block Escalation
When a whitelist transformation blocks a parameter, MODIFY escalates to DENY. The tool does not execute.
Foundation features carried into v1.2.1
Context Provenance Tracking
Each context write reported to the gate is recorded with its source, authority level, writer, timestamp and content hash.
Trust Degradation
Session trust is monotonic. Once untrusted content enters context, trust only goes down. Requires new session to reset.
Memory Gate
Controls who can read and write to agent memory, with persistence scope (none, session, cross-session) and prohibited content rules.
3 Context Authority Levels
authoritative, derived, untrusted.
Full Backward Compatibility
v1.2.1 stays fully backward compatible with all earlier policies. Existing definitions continue to work without changes.
Independent Filter Pipeline
Injection defense and PII protection run as separate, non-interfering layers. Tuning one never degrades the other.
Measured on AgentDojo
Most security tools show you their wins. Here is the whole table (gpt-4o-mini, tool_knowledge attack, single runs at the benchmark's default sampling; the attacked cells are 140 episodes on travel and 105 on slack, and the benign ceilings are 20 and 21 tasks). Cells are utility.
| suite | undefended, attacked | uniform gate (v1.3) | selective gate (v1.4) | benign ceiling |
|---|---|---|---|---|
| travel | 36.43% | 30.00% | 51.43% | 65.00% |
| slack | 52.38% | 4.76% | 4.76% | 76.19% |
Defense-effective attack success is 0.00% in every defended cell above except travel under the uniform gate, which sits at 2.14%. That residual is persuasion: three episodes whose injection goal is met in the model's text output, with no tool call to gate. Selective gating takes it to 0.00%. Banking and workspace hold 0.00% defense-effective attack success across two models in the v1.3 evaluation (paper, Table 3).
Travel is the win. Selective gating restores utility to 79% of the benign ceiling, from 46% under the uniform gate, and the defended agent beats the undefended one under attack, because blocked injections stop derailing it.
Slack is the cost, stated plainly. The defense floors utility at 4.76% whether or not an attack is present, and on slack it costs more utility than the attack it prevents. Every benign slack task reads a channel before writing, so the write-gate denies the write. Selective gating cannot fix this: slack's floor comes from taint-gated outbound and membership writes plus lineage blocks on reads, not from value-carrying writes.
The sign of the effect is set by workload shape. If your agent's benign workflow is read-then-write over attacker-reachable content, this gate is expensive, and you should know that before deploying. v1.4 ships audit_action_classes() so you can find out which case you are in from your own traffic instead of guessing.
The attack-success numbers above use defense-effective ASR, which excludes verified benchmark scoring artifacts (episodes where a blocked call is scored as a success). The underlying checker issue was reported upstream (agentdojo #168), and official ASR is disclosed alongside it in the paper. Single-run rates on this benchmark carry sampling noise; the spread across replicates is measured and reported in the benchmark notes.
What the Gate Cannot Do
Reproduced from the AgentLock README. These are the attack shapes a provenance gate does not catch.
Limits
- Persuasion. If the injection's goal is achieved in the model's text output, there is no tool call to gate. Travel's entire residual in our evaluation is this mechanism.
- Read goals. If the goal is achieved by a read, a write-gate cannot block it; parameter lineage covers the subset with distinctive values.
- Misclassification. The gate is only as correct as the authority labels on your tools. A single mislabeled tool accounted for the entire slack residual before we found it. Classification auditing is a deployment requirement, not an afterthought, which is why v1.4 ships the audit.
- Laundering that breaks carriage. As of 1.7, enforcement is bounded by carriage rather than by hop count: a value relayed through an intermediate tool is denied
param_lineageciting the relay entry, because the relaying write records the untrusted entry as its parent and decision-time checks walk the recorded link. The link is established only when the ingesting call's parameters carry the prior entry's whole content, at or above the containment floor. A value that the intermediate tool rewrites, paraphrases, or truncates below the floor, or that never reaches that call's parameters, does not link, and the chain stops there. - Recognizing an encoding of something it never saw. v1.6 matches encoded forms by encoding the untrusted values it already has and looking for them, never by decoding your parameters. That is what keeps a benign value that merely looks like base64 from being misread. The price is that a payload whose plaintext never entered the session as untrusted content has nothing to match against.
- Selection influence. Untrusted content that merely chooses among values the user already supplied plants nothing, so there is nothing for a provenance match to fire on. The session write-gate covers the gated case; parameter lineage does not.
- Guessing which call you meant. An execution token is bound to the parameters the gate authorized, the empty call included. As of 1.9,
execute()must be handed the same parametersauthorize()saw or it raisesTokenInvalidError; the gate has no way to tell which of two differing parameter sets was the honest one, so it refuses both. - Telling a legitimate quotation from an attack. A summary that genuinely quotes an attacker-supplied address really does carry that value, so it is denied. Deciding it was benign would mean judging what the value is for, which is the content judgement this gate refuses to make.
Earlier Evaluations (v1.1 and v1.2)
Both suites below were run before the provenance-lineage gate existed, and both are authored and graded in the AgentLock repo rather than by an external benchmark. They are kept for the regression history. The current evaluation is AgentDojo, above.
v1.1.2: 181 Injection Tests (published March 24, 2026)
Five-way progression (no AgentLock, v1.0, v1.1, v1.1.1, v1.1.2), tested against a LangChain agent on Gemini 2.5 Flash-Lite. Same model, same tools, same attack suite; only the middleware changed. The report includes the regressions: v1.1 broke PII protection chasing injection gains, v1.1.1 regressed injection restoring PII, v1.1.2 decoupled the two pipelines and held both.
| Metric | No AgentLock | AgentLock v1.1.2 |
|---|---|---|
| Injection tests run | 167 | 181 |
| Injection failures | 73 | 12 |
| Injection pass rate | 56% | 93.4% |
| PII findings | 3 | 0 |
| YARA threat matches | 13 | 2 |
| Categories with failures | 25 | 7 |
| Overall security score | 45/F | 66/D |
Of v1.1.2's 12 remaining failures, 9 are model-layer information leakage: the LLM confirms it has a system prompt while refusing to share it. The report attributes these to the model's decision to acknowledge, which filtering at the gate does not reach, and recommends system prompt engineering. The other 3 (AH-001, DX-005, FF-004) are behavioral edge cases where the attack partially succeeds.
v1.2.1: 222 Attack Vectors, Compromised-Admin Profile (published April 6, 2026)
Tested against Grok (xAI) as the agent model. The attacker holds valid admin credentials, so authentication and role checks pass on each call, isolating behavioral and structural defenses from role-based access control. The suite grew from 182 to 222 vectors across 35 categories between v1.2.0 and v1.2.1.
| Version measured | Pass rate | Grade | Passed |
|---|---|---|---|
| v1.1.2 (permissions only) | 30.2% | F | 55/182 |
| v1.2.0 (hardening + MODIFY/DEFER/STEP_UP) | 81.3% | B | 148/182 |
| v1.2.1 (gap fixes + signed receipts) | 99.5% | A | 221/222 |
On v1.2.1, 34 of 35 categories scored 100/A and one (social_engineering_advanced) scored 90/A. No raw PII was exfiltrated in any test, because the PII tools returned redacted output via MODIFY. The single remaining failure is a social engineering attack using legal citation pressure with no injection language.
AARM Conformance
Of the 9 AARM requirements in v1.11.0: 7 shipped, 1 partial (R6: identity binding, no service or agent identity level, no revocation check), 1 foundation only (R8).
| ID | Requirement | Status | Source |
|---|---|---|---|
| R1 | Action Mediation | SHIPPED | gate.py AuthorizationGate: DENY, DEFER, fail-closed; MCP wrapper fails closed (1.9.0) |
| R2 | Context Accumulation | SHIPPED | chain.py ContextChain, hash-chained context (1.2.1) |
| R3 | Policy Engine | SHIPPED | policy.py PolicyEngine |
| R4 | Decision Types (5) | SHIPPED | DecisionType: ALLOW, DENY, DEFER, STEP_UP, MODIFY (1.2.0) |
| R5 | Signed Receipts | SHIPPED | receipts.py ReceiptSigner, Ed25519 (1.2.1); rate-limit denials carry no receipt (1.8.0 limits) |
| R6 | Identity Binding | PARTIAL | token.py ExecutionToken binds user_id and role; verified bearer identity, unverified tokens refused (1.10.2); authorize() and execute() address a session by session_id, and a missing, expired or other user's session is denied session_not_found, never substituted (1.11.0). Human and session levels only: no separate agent or service identity, no revocation check. |
| R7 | Drift Detection | SHIPPED | Trust degradation (1.1.0) and DEFER triggers (1.2.0) |
| R8 | SIEM Export | FOUNDATION ONLY | audit.py AuditBackend protocol with file, in-memory and async backends; no SIEM exporter ships |
| R9 | Least Privilege | SHIPPED | Per-tool roles and scope, deny by default; recipient policy (1.8.0) |
AARM is a CSA working-group control model (Errico, 2026); AgentProv's provenance layers are a separate model that maps AgentLock to layer 2. agentprov.com/model
A Reference Implementation, Not a Competing Standard
AgentLock is a reference implementation of the emerging pre-action authorization consensus: a concrete, testable instance of controls that independent specs (OAP's PAA-1 through PAA-5, OWASP Agentic Top 10) are converging on. AGPL-3.0 licensed (commercial licenses available), framework-agnostic, and designed so that any agent framework can enforce security without buying anything.
| Capability | AgentLock | MS AGT | OAP | NeMo | AgentMint |
|---|---|---|---|---|---|
| Pre-action authorization gate | ✅ | ✅ | ✅ | ❌ | ⚠️ |
| Session-level compound behavioral scoring | ✅ | ❓ | ❌ | ❌ | ❌ |
| Decision types beyond allow/deny | ✅ | ✅ | ⚠️ | ⚠️ | ❌ |
| Published adversarial benchmark with regression data | ✅ | ❌ | ⚠️ | ❌ | ❌ |
| Trust degradation within session | ✅ | ❓ | ❌ | ❌ | ❌ |
| Ed25519 signed receipts | ✅ | ✅ | ❓ | ❌ | ✅ |
| Hash-chained tamper-evident audit | ⚠️ context is chained, audit log is not | ✅ | ✅ | ❌ | ✅ |
| Framework integrations (count) | 8 frameworks (4 in core, 5 standalone packages) | ~19 | ~7 | 1 | 5 |
| Language SDKs (count) | 1 | 5 | 1 | 1 | 2 |
Read this honestly: Microsoft's Agent Governance Toolkit is ahead of AgentLock on distribution and cryptographic surface: more framework integrations, more language SDKs, per-call Ed25519 receipts, a Merkle-chained audit log, and a peer decision model. Signed receipts and hash-chained audit are becoming table stakes, not differentiators. What's actually narrow and defensible about AgentLock is two things: a published adversarial benchmark that includes its own regressions (nobody else in this table shows their setbacks), and session-level compound behavioral scoring that fires on sequences of calls, not a single scalar trust score. A smaller, single-language reference implementation whose edge is rigor and behavioral analysis, not distribution.
Try It Yourself
Register a tool with a lineage policy, then authorize the same call before and after untrusted content enters the session. The tool call is identical both times. Only the provenance of the session changed.
pip install agentlock
from agentlock import (
AgentLockPermissions,
AuthorizationGate,
ContextSource,
LineagePolicyConfig,
)
gate = AuthorizationGate()
gate.register_tool("send_email", AgentLockPermissions(
risk_level="high",
allowed_roles=["user"],
lineage_policy=LineagePolicyConfig(enabled=True, decision="deny"),
))
session = gate.create_session(user_id="alice", role="user")
# The user's own instruction is authoritative.
gate.notify_context_write(
session.session_id, ContextSource.USER_MESSAGE, "hash-of-user-request",
)
first = gate.authorize(
"send_email", user_id="alice", role="user",
parameters={"to": "bob@corp.com"}, is_external=True,
)
print(first.decision.value, first.allowed) # allow True
# A web page enters the session. Nothing about the tool call changes.
gate.notify_context_write(
session.session_id, ContextSource.WEB_CONTENT, "hash-of-web-page",
tool_name="fetch_url",
)
second = gate.authorize(
"send_email", user_id="alice", role="user",
parameters={"to": "bob@corp.com"}, is_external=True,
)
print(second.decision.value, second.allowed) # deny False
print(second.denial["reason"]) # untrusted_lineageRoadmap
What has shipped, then what is planned. Planned items are not in any released version.
Shipped
Tool Permissions
SHIPPEDDeclarative authorization blocks, single-use tokens, rate limiting, data redaction, audit trail.
Context Authority & Memory Gate
SHIPPEDContext authority model with trust degradation, provenance tracking, memory access control. Independent injection and PII filter pipeline.
Adaptive Hardening & Decision Types
SHIPPEDAdaptive hardening, MODIFY/DEFER/STEP_UP decisions, Ed25519 signed receipts, hash-chained context, multi-signal detection. 847 tests.
Provenance-Lineage Gating & Deferred Commit
SHIPPEDSession write-gate, parameter lineage, deferred commit. Evaluated on AgentDojo. 868 tests.
Selective Action-Class Gating & Novel Lineage
SHIPPEDPer-tool action-class declarations, novel lineage, action-class audit, needs_approval at the gate boundary. Schema v1.4. 1041 tests.
Evidence Layer
SHIPPEDGrant basis recording, execution confirmation, provenance on denials, deferred-resolution logging. Records strictly more and decides identically, verified by A/B replay with zero decision diffs across 4542 replayable decisions. LangChain and CrewAI integrations extracted into standalone packages (langchain-agentlock, crewai-agentlock). 1141 tests.
Derivation Taint
SHIPPEDValue-identity normalization and encoded-form attribution (base64, hex, natural-URL), bare and composite, without decoding. 1364 tests.
Cross-Hop Provenance Linking
SHIPPEDA derived entry whose ingestion carries a prior entry's whole content records that entry as its parent, and decision-time checks walk those links with a cycle-guarded taint-reachability walk. A value relayed through an intermediate tool and then consumed is denied with reason param_lineage, citing the relay entry as the untrusted origin. Live in the crewai and mcp adapters. 1418 tests passing with the [crypto,mcp] extras.
Recipient Policy Enforcement
SHIPPEDallowed_recipients enforced at Step 8 for blocks at schema version 1.5 or later: known_contacts_only, allowlist, same_domain, any. The block declares recipient_parameter and the gate reads the recipient from the tool's own arguments. Schema v1.5. 1495 tests passing with the [crypto,mcp] extras.
Enforcement & Binding Completeness
SHIPPEDAll call arguments reach the gate, tokens bind the empty call, and the MCP wrapper fails closed and supports both SDK majors (1.9.0). A **kwargs key that names another parameter is refused, partials are bound through to the function underneath, recipients are read for the characters they hold, and an unobservable declared recipient parameter is refused (1.9.1). 1520 tests passing with the [crypto,mcp] extras.
Integration Hardening & Verified Identity
SHIPPEDOne execution contract, so a declared transformation reaches the tool and the caller on each in-repo execution path (the HTTP adapters do not call execute()); the authenticated session's role is authoritative over the caller's claim (role_mismatch); resolved path containment; parameter and novel lineage re-checked at deferred commit (1.10.0, 1.10.1). A bearer token carries identity only once it has been verified against a configured key (1.10.2). Opt-in gate_derived_origin escalates derived-origin external actions to needs_approval (1.10.3). Schema v1.5 unchanged. 1834 tests passing with the [crypto,mcp] extras plus fastapi, flask and python-jose.
Review Gaps Closed by Measurement
SHIPPEDFrom an external review of the 1.10.3 wheel: a deferred action is re-checked at commit for registration, role, session liveness and policy version; authorize() and execute() address a session by session_id, and a second live session per user can be refused with the opt-in single_session_per_user; destroying a superseded session no longer orphans the live one; the HTTP adapters authorize the JSON body and fail closed on other content types; redaction reaches every str leaf of structured results on every path. 1912 tests passing with the [crypto,mcp] extras plus fastapi, flask and python-jose, 9 skipped and 1 expected failure.
Planned, not shipped
As of October 29, 2026. None of the following is in AgentLock v1.11.0.
Execution Scope & Behavioral Policy (v2.0)
PLANNEDRestrict where agent outputs can be sent: channels, APIs, and storage destinations. A full behavioral policy engine that constrains what agents can do, not just which tools they can call.
gate_derived_origin on by default (v2.0)
PLANNEDThe 1.10.3 changelog names enabling the derived-origin escalation by default as the intended change for 2.0. It is off by default in 1.10.x and 1.11.0.
single_session_per_user on by default (v2.0)
PLANNEDThe 1.11.0 changelog names refusing a second live session per user as the default for 2.0. It is opt-in and off by default in 1.11.0.
Scoped, out-of-band authorization (v2)
PLANNEDDeferred to v2 in the 1.10.3 changelog: letting a benign derived-origin action through on evidence of authorized intent captured outside the natural-language channel. No call sites exist in the package today.
Compliance report templates
PLANNEDTemplates for SOC 2, HIPAA, the EU AI Act, and SR 11-7.
Standards alignment mappings
PLANNEDPublished mappings from AgentLock controls to these frameworks. No mapping ships with v1.11.0.
Research
Published on Zenodo with DOIs; not peer reviewed.
Related work: runwarden, a cross-run isolation probe (Grice, 2026). DOI: 10.5281/zenodo.22679860
Software archive: https://doi.org/10.5281/zenodo.22681594 (all versions).
ORCID: 0009-0005-5388-123X
Frequently Asked Questions
What is AgentLock?
AgentLock is an open-source, adversarially benchmarked reference implementation for pre-action AI agent authorization. Deny-by-default tool permissions, signed receipts, audit logging. AGPL-3.0 (commercial licenses available). pip install agentlock.
Is AgentLock free?
Yes. AgentLock is AGPL-3.0 licensed and free to use. Commercial licenses are available for organizations that cannot adopt AGPL.
Is this the same as agentlock.net or the AgentLock iOS app?
No. AgentLock (agentlock.dev), created by David Grice, is not affiliated with agentlock.net, the AgentLock iOS app on the App Store, or GiliSoft's AI Agent Lock. This project is the open-source Python framework at github.com/webpro255/agentlock and has no official mobile app.