agentlock 1.1.2__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentlock-1.2.0/CHANGELOG.md +135 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/PKG-INFO +53 -4
- {agentlock-1.1.2 → agentlock-1.2.0}/README.md +52 -3
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/__init__.py +58 -1
- agentlock-1.2.0/agentlock/defer.py +211 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/exceptions.py +64 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/gate.py +422 -8
- agentlock-1.2.0/agentlock/hardening.py +451 -0
- agentlock-1.2.0/agentlock/modify.py +242 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/redaction.py +10 -1
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/schema.py +73 -1
- agentlock-1.2.0/agentlock/signals/__init__.py +30 -0
- agentlock-1.2.0/agentlock/signals/combos.py +205 -0
- agentlock-1.2.0/agentlock/signals/echo.py +277 -0
- agentlock-1.2.0/agentlock/signals/prompt_scan.py +383 -0
- agentlock-1.2.0/agentlock/signals/velocity.py +221 -0
- agentlock-1.2.0/agentlock/stepup.py +255 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/types.py +11 -0
- agentlock-1.2.0/docs/benchmark.md +385 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/pyproject.toml +1 -1
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_backward_compat.py +4 -4
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_cli.py +1 -1
- agentlock-1.2.0/tests/test_combos.py +239 -0
- agentlock-1.2.0/tests/test_compound.py +150 -0
- agentlock-1.2.0/tests/test_defer.py +248 -0
- agentlock-1.2.0/tests/test_echo.py +249 -0
- agentlock-1.2.0/tests/test_gate_v12.py +330 -0
- agentlock-1.2.0/tests/test_hardening.py +635 -0
- agentlock-1.2.0/tests/test_modify.py +337 -0
- agentlock-1.2.0/tests/test_prompt_scan.py +269 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_redaction.py +53 -0
- agentlock-1.2.0/tests/test_stepup.py +326 -0
- agentlock-1.2.0/tests/test_velocity.py +166 -0
- agentlock-1.1.2/CHANGELOG.md +0 -62
- {agentlock-1.1.2 → agentlock-1.2.0}/.github/workflows/ci.yml +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/.gitignore +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/CONTRIBUTING.md +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/LICENSE +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/NOTICE +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/SECURITY.md +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/audit.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/auth_providers/__init__.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/cli.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/context.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/decorators.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/__init__.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/autogen.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/crewai.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/fastapi.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/flask.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/langchain.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/mcp.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/memory_gate.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/policy.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/py.typed +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/rate_limit.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/session.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/token.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/docs/integrations.md +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/docs/quickstart.md +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/docs/specification.md +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/examples/data_redaction.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/examples/decorator_example.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/examples/fastapi_app.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/examples/multi_role.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/examples/quickstart.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/examples/rate_limiting.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/schema/agentlock-v1.0.json +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/__init__.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/conftest.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_audit.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_audit_v11.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_context.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_decorators.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_filter_pipeline.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_gate.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_gate_v11.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_memory_gate.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_pii_defense.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_policy.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_policy_v11.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_rate_limit.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_schema.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_schema_v11.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_session.py +0 -0
- {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_token.py +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to AgentLock will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.2.0] - 2026-03-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Adaptive prompt hardening** -- When the gate detects suspicious activity (injection attempts, trust degradation, rate limiting), it generates defensive system prompt instructions for the agent framework to inject before the LLM processes the next turn. Session risk scores are monotonic and session-scoped. Three severity levels: warning, elevated, critical.
|
|
13
|
+
- **MODIFY decision type** -- Authorized tool calls can have their outputs transformed before the LLM sees them. Built-in actions: `redact_pii` (strips SSN, email, phone, credit card, API keys from output), `restrict_domain` (blocks external email recipients), `whitelist_path` (restricts file access to allowed directories), `cap_records` (limits output record count). Configured per-tool via `modify_policy`.
|
|
14
|
+
- **DEFER decision type** -- Suspends authorization when context is ambiguous. Triggers: first tool call in session is HIGH/CRITICAL risk with no history, prompt scanner fired and tool call attempted in the same turn, trust degraded below threshold. Defaults to DENY on timeout (60s).
|
|
15
|
+
- **STEP_UP decision type** -- Dynamically requires human approval based on session state. Triggers: hardening severity at elevated or above with HIGH/CRITICAL risk tool, multiple PII-returning tools already called in session, tool denied earlier and user retrying with a different high-risk tool. Pluggable notification via `StepUpNotifier` protocol.
|
|
16
|
+
- **DecisionType enum** -- Five authorization outcomes: `ALLOW`, `DENY`, `DEFER`, `STEP_UP`, `MODIFY`. `AuthResult.decision` field added alongside backward-compatible `AuthResult.allowed`.
|
|
17
|
+
- **Gate enforcement at critical severity** -- When session risk score exceeds the critical threshold (10+) and `enforce_at_critical` is enabled, the gate blocks HIGH/CRITICAL risk tools regardless of role authorization. MEDIUM/LOW tools remain allowed.
|
|
18
|
+
- **Prompt scanner** (`PromptScanner`) -- Pre-LLM analysis of user messages. Detects injection phrases, authority claims, instruction planting, encoding indicators, agent/system impersonation, format forcing, retrieval exploitation, and cross-turn repetition. Runs before the LLM processes the message, enabling hardening directives on the same turn.
|
|
19
|
+
- **Behavioral velocity detector** (`VelocityDetector`) -- Tracks tool call frequency and topic shifts per session. Fires on rapid calls (3+ in 60s), topic escalation (risk jump from low/medium to high/critical), and burst patterns (same tool 3+ in 30s).
|
|
20
|
+
- **Tool combination detector** (`ComboDetector`) -- Detects suspicious tool call sequences within a session. Configurable suspicion map with 13 default suspicious pairs and 5 default suspicious sequences covering data exfiltration, account takeover, and tool chain attack patterns.
|
|
21
|
+
- **Response echo detector** (`EchoDetector`) -- Framework-side signal that checks LLM responses for attack prompt echoing, tool name disclosure, system prompt leakage, credential-format strings, and compliance language in suspicious contexts.
|
|
22
|
+
- **Compound scoring** -- When multiple signal types co-occur, compound rules add bonus weight. `rapid_exfil` (velocity + combo, +2), `probing_attack` (echo + injection, +3).
|
|
23
|
+
- **Signal-aware targeted instructions** -- Hardening directives contain instructions specific to the detected signal types instead of generic severity-level text. Format forcing attacks get format-specific instructions, not irrelevant tool-blocking language.
|
|
24
|
+
- New schema models: `ModifyPolicyConfig`, `TransformationConfig`, `DeferPolicyConfig`, `StepUpPolicyConfig`
|
|
25
|
+
- New exceptions: `DeferredError`, `StepUpRequiredError`, `ModifyAppliedError`
|
|
26
|
+
- 276 new tests (745 total, 0 failures)
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Schema version updated from `"1.1"` to `"1.2"`
|
|
31
|
+
- Package version updated to `1.2.0`
|
|
32
|
+
- Phone number redaction pattern expanded to cover 7-digit, US 10-digit, international (+44, +91, +1), and UK local (0-prefixed) formats
|
|
33
|
+
- `AuthorizationGate.__init__()` accepts optional `hardening_config`, `velocity_config`, `combo_config`
|
|
34
|
+
- `AuthorizationGate.execute()` accepts optional `modify_output_fn` for MODIFY output transformation
|
|
35
|
+
- `AuthorizationGate.authorize()` pipeline extended: velocity/combo signals recorded before policy evaluation, DEFER checked before STEP_UP, STEP_UP checked before MODIFY, MODIFY checked before token issuance
|
|
36
|
+
|
|
37
|
+
### Backward Compatibility
|
|
38
|
+
|
|
39
|
+
- All v1.0 and v1.1.x `agentlock` permission blocks remain valid
|
|
40
|
+
- `AuthResult.allowed` continues to work unchanged for existing callers
|
|
41
|
+
- New fields (`decision`, `modify_output_fn`, `deferral_id`, `stepup_request_id`) default to neutral values
|
|
42
|
+
- `execute()` works identically without the `modify_output_fn` parameter
|
|
43
|
+
- Hardening, velocity, combo, DEFER, STEP_UP, and MODIFY are all disabled by default when their respective config/policy objects are not provided
|
|
44
|
+
- All 469 original v1.1.2 tests pass without modification
|
|
45
|
+
|
|
46
|
+
## [1.1.2] - 2026-03-24
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Independent filter pipeline** — Decoupled InjectionFilter and PiiFilter into separate classes on PolicyEngine. Each runs independently with no shared logic or state.
|
|
51
|
+
- **InjectionFilter** — Scans tool call parameters for reconnaissance/enumeration, prompt extraction, social engineering, and command injection patterns. Recursively inspects nested dicts and lists.
|
|
52
|
+
- **PiiFilter** — Checks caller's max_output_classification against tool's output_classification using 7-level classification hierarchy. Independent from injection filtering.
|
|
53
|
+
- 44 new tests (test_filter_pipeline.py)
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- PolicyEngine.evaluate() refactored into three independent stages: base auth, injection filter, PII filter
|
|
58
|
+
- Trust degradation now runs independently of both filters
|
|
59
|
+
- Package version updated to 1.1.2
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- Injection pass rate recovered from 88.6% (v1.1.1) to 93.4% by restoring behavioral filters without PII interference
|
|
64
|
+
|
|
65
|
+
## [1.1.1] - 2026-03-24
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **Gate-level PII classification check** — max_output_classification parameter on authorize() blocks tool execution before data is retrieved when caller clearance is below tool's output classification
|
|
70
|
+
- 7-level classification hierarchy: PUBLIC, INTERNAL, CONFIDENTIAL, MAY_CONTAIN_PII, CONTAINS_PII, CONTAINS_PHI, CONTAINS_FINANCIAL
|
|
71
|
+
- 16 new tests (test_pii_defense.py)
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- PII regression from v1.1: restored input-layer query blocking (100/A) while maintaining output-layer redaction as backup
|
|
76
|
+
|
|
77
|
+
### Backward Compatibility
|
|
78
|
+
|
|
79
|
+
- max_output_classification defaults to None. When not provided, check is skipped entirely. No existing callers affected.
|
|
80
|
+
|
|
81
|
+
## [1.1.0] - 2026-03-20
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- **Context authority model** — `context_policy` block on `AgentLockPermissions` with `source_authorities` mapping context sources (user messages, tool outputs, web content, peer agents, etc.) to authority levels (`authoritative`, `derived`, `untrusted`)
|
|
86
|
+
- **Trust degradation** — `TrustDegradationConfig` with per-session trust that monotonically degrades when untrusted content enters context. Effects: `require_approval`, `elevate_logging`, `restrict_scope`, `deny_writes`. Trust never escalates within a session.
|
|
87
|
+
- **`allow_cascade_to_untrusted`** flag for security-critical deployments that need maximum restriction after contamination
|
|
88
|
+
- **Memory access control** — `memory_policy` block with `allowed_writers`, `allowed_readers`, `prohibited_content`, `retention` limits, and `require_write_confirmation`
|
|
89
|
+
- **Provenance tracking** — `ContextProvenance` dataclass with source, authority, writer identity, timestamp, content hash, and token binding for every context write
|
|
90
|
+
- **`ContextTracker`** — per-session provenance log and trust state management on the authorization gate
|
|
91
|
+
- **`MemoryGate`** — validates memory read/write operations against `MemoryPolicyConfig` with lazy retention enforcement
|
|
92
|
+
- **`notify_context_write()`** on `AuthorizationGate` — framework integrations report context entries to the gate
|
|
93
|
+
- **`authorize_memory_write()` / `authorize_memory_read()`** on `AuthorizationGate`
|
|
94
|
+
- **New enums**: `ContextSource`, `ContextAuthority`, `DegradationEffect`, `MemoryPersistence`, `MemoryWriter`
|
|
95
|
+
- **New denial reasons**: `TRUST_DEGRADED`, `UNATTRIBUTED_CONTEXT`, `CONTEXT_AUTHORITY_VIOLATION`, `MEMORY_WRITE_DENIED`, `MEMORY_READ_DENIED`, `MEMORY_RETENTION_EXCEEDED`, `MEMORY_PROHIBITED_CONTENT`, `MEMORY_CONFIRMATION_REQUIRED`
|
|
96
|
+
- **New audit actions**: `trust_degraded`, `memory_write`, `memory_write_denied`, `memory_read`, `memory_read_denied`, `memory_expired`, `context_rejected`
|
|
97
|
+
- **New audit fields**: `trust_ceiling`, `is_trust_degraded`, `degradation_effects`, `context_provenance_ids`, `memory_operation`, `memory_entry_id`
|
|
98
|
+
- **New exception classes**: `TrustDegradedError`, `UnattributedContextError`, `MemoryWriteDeniedError`, `MemoryReadDeniedError`, `MemoryRetentionExceededError`, `MemoryProhibitedContentError`, `MemoryConfirmationRequiredError`
|
|
99
|
+
- CLI `validate` and `inspect` commands now display v1.1 context and memory policy fields
|
|
100
|
+
- `agentlock init` now generates v1.1 templates
|
|
101
|
+
- 142 new tests (409 total)
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- Schema version default updated from `"1.0"` to `"1.1"`
|
|
106
|
+
- Package version updated to `1.1.0`
|
|
107
|
+
|
|
108
|
+
### Backward Compatibility
|
|
109
|
+
|
|
110
|
+
- All v1.0 `agentlock` blocks remain valid — new fields are optional with secure defaults
|
|
111
|
+
- When `version` is `"1.0"`, the gate skips all v1.1 checks entirely
|
|
112
|
+
- All 267 original tests continue to pass without modification
|
|
113
|
+
|
|
114
|
+
## [1.0.0] - 2026-03-18
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- Core AgentLock permissions schema (v1.0)
|
|
119
|
+
- `AuthorizationGate` central enforcement point with deny-by-default semantics
|
|
120
|
+
- `AgentLockPermissions` Pydantic model for the `agentlock` permissions block
|
|
121
|
+
- `@agentlock` decorator for one-line tool protection
|
|
122
|
+
- Single-use, time-limited, operation-bound execution tokens
|
|
123
|
+
- Session management with expiry and scope tracking
|
|
124
|
+
- Sliding-window per-user, per-tool rate limiting
|
|
125
|
+
- Automatic data redaction engine with built-in PII patterns
|
|
126
|
+
- Policy evaluation engine with 7-step authorization checks
|
|
127
|
+
- Pluggable audit logging with file and in-memory backends
|
|
128
|
+
- CLI tool: `agentlock validate`, `agentlock schema`, `agentlock init`, `agentlock inspect`, `agentlock audit`
|
|
129
|
+
- Framework integrations: LangChain, CrewAI, AutoGen, MCP, FastAPI, Flask
|
|
130
|
+
- JSON Schema for tool definition validation
|
|
131
|
+
- Comprehensive test suite
|
|
132
|
+
- Working examples for all major use cases
|
|
133
|
+
- Full documentation
|
|
134
|
+
- GitHub Actions CI/CD pipeline
|
|
135
|
+
- Apache 2.0 license
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentlock
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Authorization framework for AI agent tool calls. Your AI agent needs a login screen — AgentLock is that login screen.
|
|
5
5
|
Project-URL: Homepage, https://agentlock.dev
|
|
6
6
|
Project-URL: Documentation, https://agentlock.dev/docs
|
|
@@ -301,7 +301,7 @@ agentlock audit --tool send_email # Query audit logs
|
|
|
301
301
|
|
|
302
302
|
## What AgentLock Prevents
|
|
303
303
|
|
|
304
|
-
Based on empirical research:
|
|
304
|
+
Based on empirical research: multi-turn adversarial attack testing across 35 categories, tested against multiple frontier AI models.
|
|
305
305
|
|
|
306
306
|
| Attack Category | Prevention |
|
|
307
307
|
|----------------|-----------|
|
|
@@ -375,6 +375,55 @@ gate.register_tool("assistant", AgentLockPermissions(
|
|
|
375
375
|
|
|
376
376
|
Every write to context generates a `ContextProvenance` record with source, authority, writer identity, timestamp, and content hash. Audit records now include `trust_ceiling`, `context_provenance_ids`, and `memory_operation` fields.
|
|
377
377
|
|
|
378
|
+
## v1.2: Adaptive Hardening & New Decision Types
|
|
379
|
+
|
|
380
|
+
AgentLock v1.2 adds four capabilities that close the gap between authorization and runtime defense.
|
|
381
|
+
|
|
382
|
+
### Adaptive Prompt Hardening
|
|
383
|
+
|
|
384
|
+
When the gate detects suspicious activity, it generates defensive instructions for the agent's system prompt. A pre-LLM prompt scanner analyzes user messages before the model processes them, enabling hardening on the first turn of an attack. Four signal detectors (velocity, tool combination, response echo, prompt scan) feed into a monotonic session risk score.
|
|
385
|
+
|
|
386
|
+
### Five Decision Types
|
|
387
|
+
|
|
388
|
+
v1.0/v1.1 supported ALLOW and DENY. v1.2 adds three more:
|
|
389
|
+
|
|
390
|
+
| Decision | When | Effect |
|
|
391
|
+
|----------|------|--------|
|
|
392
|
+
| **ALLOW** | Call is authorized | Token issued, tool executes normally |
|
|
393
|
+
| **DENY** | Call is not authorized | No token, structured denial returned |
|
|
394
|
+
| **MODIFY** | Call is authorized but output must be transformed | Token issued, PII redacted from output before LLM sees it |
|
|
395
|
+
| **DEFER** | Context is ambiguous, gate cannot decide | Action suspended, resolves via human review or timeout |
|
|
396
|
+
| **STEP_UP** | Session state indicates elevated risk | Action paused, human approval required |
|
|
397
|
+
|
|
398
|
+
### MODIFY: Output Transformation
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
gate.register_tool("query_database", AgentLockPermissions(
|
|
402
|
+
risk_level="high",
|
|
403
|
+
requires_auth=True,
|
|
404
|
+
allowed_roles=["admin", "support"],
|
|
405
|
+
modify_policy=ModifyPolicyConfig(
|
|
406
|
+
enabled=True,
|
|
407
|
+
transformations=[
|
|
408
|
+
TransformationConfig(field="output", action="redact_pii"),
|
|
409
|
+
TransformationConfig(
|
|
410
|
+
field="to", action="restrict_domain",
|
|
411
|
+
config={"allowed_domains": ["company.com"]},
|
|
412
|
+
),
|
|
413
|
+
],
|
|
414
|
+
),
|
|
415
|
+
))
|
|
416
|
+
|
|
417
|
+
result = gate.authorize("query_database", user_id="alice", role="admin")
|
|
418
|
+
# result.decision == DecisionType.MODIFY
|
|
419
|
+
# result.modify_output_fn strips PII from tool output before the LLM sees it
|
|
420
|
+
output = gate.execute("query_database", db_func, token=result.token,
|
|
421
|
+
modify_output_fn=result.modify_output_fn)
|
|
422
|
+
# output: {'name': 'Jane Doe', 'email': '[REDACTED:email]', 'ssn': '[REDACTED:ssn]'}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The tool still executes. The admin still gets the answer. But PII never enters the LLM context where it can be weaponized by injection attacks.
|
|
426
|
+
|
|
378
427
|
## Standards Alignment
|
|
379
428
|
|
|
380
429
|
| Standard | Coverage |
|
|
@@ -391,8 +440,8 @@ Every write to context generates a `ContextProvenance` record with source, autho
|
|
|
391
440
|
| Version | Focus |
|
|
392
441
|
|---------|-------|
|
|
393
442
|
| **v1.0** | Core schema, tool permissions, enforcement architecture |
|
|
394
|
-
| **v1.1** | Memory/context permissions, trust degradation, provenance tracking
|
|
395
|
-
| **v1.2** |
|
|
443
|
+
| **v1.1** | Memory/context permissions, trust degradation, provenance tracking |
|
|
444
|
+
| **v1.2** | Adaptive hardening, MODIFY/DEFER/STEP_UP decisions, multi-signal detection (745 tests) |
|
|
396
445
|
| **v1.3** | Output destination control, data flow policies |
|
|
397
446
|
| **v2.0** | Execution scope, behavioral policy, anomaly detection, compliance templates |
|
|
398
447
|
|
|
@@ -242,7 +242,7 @@ agentlock audit --tool send_email # Query audit logs
|
|
|
242
242
|
|
|
243
243
|
## What AgentLock Prevents
|
|
244
244
|
|
|
245
|
-
Based on empirical research:
|
|
245
|
+
Based on empirical research: multi-turn adversarial attack testing across 35 categories, tested against multiple frontier AI models.
|
|
246
246
|
|
|
247
247
|
| Attack Category | Prevention |
|
|
248
248
|
|----------------|-----------|
|
|
@@ -316,6 +316,55 @@ gate.register_tool("assistant", AgentLockPermissions(
|
|
|
316
316
|
|
|
317
317
|
Every write to context generates a `ContextProvenance` record with source, authority, writer identity, timestamp, and content hash. Audit records now include `trust_ceiling`, `context_provenance_ids`, and `memory_operation` fields.
|
|
318
318
|
|
|
319
|
+
## v1.2: Adaptive Hardening & New Decision Types
|
|
320
|
+
|
|
321
|
+
AgentLock v1.2 adds four capabilities that close the gap between authorization and runtime defense.
|
|
322
|
+
|
|
323
|
+
### Adaptive Prompt Hardening
|
|
324
|
+
|
|
325
|
+
When the gate detects suspicious activity, it generates defensive instructions for the agent's system prompt. A pre-LLM prompt scanner analyzes user messages before the model processes them, enabling hardening on the first turn of an attack. Four signal detectors (velocity, tool combination, response echo, prompt scan) feed into a monotonic session risk score.
|
|
326
|
+
|
|
327
|
+
### Five Decision Types
|
|
328
|
+
|
|
329
|
+
v1.0/v1.1 supported ALLOW and DENY. v1.2 adds three more:
|
|
330
|
+
|
|
331
|
+
| Decision | When | Effect |
|
|
332
|
+
|----------|------|--------|
|
|
333
|
+
| **ALLOW** | Call is authorized | Token issued, tool executes normally |
|
|
334
|
+
| **DENY** | Call is not authorized | No token, structured denial returned |
|
|
335
|
+
| **MODIFY** | Call is authorized but output must be transformed | Token issued, PII redacted from output before LLM sees it |
|
|
336
|
+
| **DEFER** | Context is ambiguous, gate cannot decide | Action suspended, resolves via human review or timeout |
|
|
337
|
+
| **STEP_UP** | Session state indicates elevated risk | Action paused, human approval required |
|
|
338
|
+
|
|
339
|
+
### MODIFY: Output Transformation
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
gate.register_tool("query_database", AgentLockPermissions(
|
|
343
|
+
risk_level="high",
|
|
344
|
+
requires_auth=True,
|
|
345
|
+
allowed_roles=["admin", "support"],
|
|
346
|
+
modify_policy=ModifyPolicyConfig(
|
|
347
|
+
enabled=True,
|
|
348
|
+
transformations=[
|
|
349
|
+
TransformationConfig(field="output", action="redact_pii"),
|
|
350
|
+
TransformationConfig(
|
|
351
|
+
field="to", action="restrict_domain",
|
|
352
|
+
config={"allowed_domains": ["company.com"]},
|
|
353
|
+
),
|
|
354
|
+
],
|
|
355
|
+
),
|
|
356
|
+
))
|
|
357
|
+
|
|
358
|
+
result = gate.authorize("query_database", user_id="alice", role="admin")
|
|
359
|
+
# result.decision == DecisionType.MODIFY
|
|
360
|
+
# result.modify_output_fn strips PII from tool output before the LLM sees it
|
|
361
|
+
output = gate.execute("query_database", db_func, token=result.token,
|
|
362
|
+
modify_output_fn=result.modify_output_fn)
|
|
363
|
+
# output: {'name': 'Jane Doe', 'email': '[REDACTED:email]', 'ssn': '[REDACTED:ssn]'}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The tool still executes. The admin still gets the answer. But PII never enters the LLM context where it can be weaponized by injection attacks.
|
|
367
|
+
|
|
319
368
|
## Standards Alignment
|
|
320
369
|
|
|
321
370
|
| Standard | Coverage |
|
|
@@ -332,8 +381,8 @@ Every write to context generates a `ContextProvenance` record with source, autho
|
|
|
332
381
|
| Version | Focus |
|
|
333
382
|
|---------|-------|
|
|
334
383
|
| **v1.0** | Core schema, tool permissions, enforcement architecture |
|
|
335
|
-
| **v1.1** | Memory/context permissions, trust degradation, provenance tracking
|
|
336
|
-
| **v1.2** |
|
|
384
|
+
| **v1.1** | Memory/context permissions, trust degradation, provenance tracking |
|
|
385
|
+
| **v1.2** | Adaptive hardening, MODIFY/DEFER/STEP_UP decisions, multi-signal detection (745 tests) |
|
|
337
386
|
| **v1.3** | Output destination control, data flow policies |
|
|
338
387
|
| **v2.0** | Execution scope, behavioral policy, anomaly detection, compliance templates |
|
|
339
388
|
|
|
@@ -34,13 +34,16 @@ Copyright 2026 David Grice
|
|
|
34
34
|
SPDX-License-Identifier: Apache-2.0
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "1.
|
|
37
|
+
__version__ = "1.2.0"
|
|
38
38
|
|
|
39
39
|
from agentlock.audit import AuditLogger, AuditRecord, FileAuditBackend, InMemoryAuditBackend
|
|
40
40
|
from agentlock.context import ContextProvenance, ContextState, ContextTracker
|
|
41
41
|
from agentlock.decorators import agentlock
|
|
42
42
|
from agentlock.exceptions import (
|
|
43
43
|
AgentLockError,
|
|
44
|
+
DeferredError,
|
|
45
|
+
ModifyAppliedError,
|
|
46
|
+
StepUpRequiredError,
|
|
44
47
|
ApprovalRequiredError,
|
|
45
48
|
AuthenticationRequiredError,
|
|
46
49
|
ConfigurationError,
|
|
@@ -62,7 +65,26 @@ from agentlock.exceptions import (
|
|
|
62
65
|
TrustDegradedError,
|
|
63
66
|
UnattributedContextError,
|
|
64
67
|
)
|
|
68
|
+
from agentlock.defer import DeferralManager, DeferralRecord
|
|
65
69
|
from agentlock.gate import AuthorizationGate, AuthResult
|
|
70
|
+
from agentlock.stepup import StepUpManager, StepUpNotifier, StepUpRequest
|
|
71
|
+
from agentlock.modify import ModifyEngine, ModifyResult
|
|
72
|
+
from agentlock.hardening import (
|
|
73
|
+
HardeningConfig,
|
|
74
|
+
HardeningDirective,
|
|
75
|
+
HardeningEngine,
|
|
76
|
+
HardeningSignal,
|
|
77
|
+
)
|
|
78
|
+
from agentlock.signals import (
|
|
79
|
+
ComboDetector,
|
|
80
|
+
ComboSignal,
|
|
81
|
+
EchoDetector,
|
|
82
|
+
EchoSignal,
|
|
83
|
+
PromptScanConfig,
|
|
84
|
+
PromptScanner,
|
|
85
|
+
VelocityDetector,
|
|
86
|
+
VelocitySignal,
|
|
87
|
+
)
|
|
66
88
|
from agentlock.memory_gate import InMemoryMemoryStore, MemoryDecision, MemoryEntry, MemoryGate
|
|
67
89
|
from agentlock.policy import (
|
|
68
90
|
InjectionFilter,
|
|
@@ -76,6 +98,10 @@ from agentlock.redaction import RedactionEngine, RedactionResult
|
|
|
76
98
|
from agentlock.schema import (
|
|
77
99
|
SCHEMA_VERSION,
|
|
78
100
|
AgentLockPermissions,
|
|
101
|
+
DeferPolicyConfig,
|
|
102
|
+
ModifyPolicyConfig,
|
|
103
|
+
StepUpPolicyConfig,
|
|
104
|
+
TransformationConfig,
|
|
79
105
|
AuditConfig,
|
|
80
106
|
ContextPolicyConfig,
|
|
81
107
|
DataPolicyConfig,
|
|
@@ -94,6 +120,7 @@ from agentlock.session import Session, SessionStore
|
|
|
94
120
|
from agentlock.token import ExecutionToken, TokenStore
|
|
95
121
|
from agentlock.types import (
|
|
96
122
|
ApprovalChannel,
|
|
123
|
+
DecisionType,
|
|
97
124
|
ApprovalThreshold,
|
|
98
125
|
AuditLogLevel,
|
|
99
126
|
AuthMethod,
|
|
@@ -125,6 +152,10 @@ __all__ = [
|
|
|
125
152
|
"SessionConfig",
|
|
126
153
|
"AuditConfig",
|
|
127
154
|
"HumanApprovalConfig",
|
|
155
|
+
"DeferPolicyConfig",
|
|
156
|
+
"ModifyPolicyConfig",
|
|
157
|
+
"StepUpPolicyConfig",
|
|
158
|
+
"TransformationConfig",
|
|
128
159
|
"SCHEMA_VERSION",
|
|
129
160
|
# v1.1 schema components
|
|
130
161
|
"ContextPolicyConfig",
|
|
@@ -164,7 +195,18 @@ __all__ = [
|
|
|
164
195
|
# Redaction
|
|
165
196
|
"RedactionEngine",
|
|
166
197
|
"RedactionResult",
|
|
198
|
+
# DEFER (v1.2)
|
|
199
|
+
"DeferralManager",
|
|
200
|
+
"DeferralRecord",
|
|
201
|
+
# STEP_UP (v1.2)
|
|
202
|
+
"StepUpManager",
|
|
203
|
+
"StepUpRequest",
|
|
204
|
+
"StepUpNotifier",
|
|
205
|
+
# MODIFY (v1.2)
|
|
206
|
+
"ModifyEngine",
|
|
207
|
+
"ModifyResult",
|
|
167
208
|
# Enums
|
|
209
|
+
"DecisionType",
|
|
168
210
|
"RiskLevel",
|
|
169
211
|
"AuthMethod",
|
|
170
212
|
"DataClassification",
|
|
@@ -181,8 +223,23 @@ __all__ = [
|
|
|
181
223
|
"MemoryWriter",
|
|
182
224
|
"DenialReason",
|
|
183
225
|
"TokenStatus",
|
|
226
|
+
# Hardening
|
|
227
|
+
"HardeningEngine",
|
|
228
|
+
"HardeningDirective",
|
|
229
|
+
"HardeningSignal",
|
|
230
|
+
"HardeningConfig",
|
|
231
|
+
# Signals
|
|
232
|
+
"VelocityDetector",
|
|
233
|
+
"VelocitySignal",
|
|
234
|
+
"ComboDetector",
|
|
235
|
+
"ComboSignal",
|
|
236
|
+
"EchoDetector",
|
|
237
|
+
"EchoSignal",
|
|
238
|
+
"PromptScanner",
|
|
239
|
+
"PromptScanConfig",
|
|
184
240
|
# Exceptions
|
|
185
241
|
"AgentLockError",
|
|
242
|
+
"ModifyAppliedError",
|
|
186
243
|
"DeniedError",
|
|
187
244
|
"AuthenticationRequiredError",
|
|
188
245
|
"InsufficientRoleError",
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""DEFER decision type — suspend authorization pending resolution.
|
|
2
|
+
|
|
3
|
+
When the gate cannot confidently allow or deny a tool call, DEFER
|
|
4
|
+
suspends execution. The deferral times out to DENY by default.
|
|
5
|
+
|
|
6
|
+
Triggers:
|
|
7
|
+
|
|
8
|
+
1. ``first_call_high_risk``: The session's very first tool call is
|
|
9
|
+
HIGH or CRITICAL risk with zero prior history.
|
|
10
|
+
2. ``scan_plus_tool``: The prompt scanner fired a signal on the
|
|
11
|
+
current turn AND the LLM is attempting a tool call.
|
|
12
|
+
3. ``trust_below_threshold``: Session trust has degraded below
|
|
13
|
+
DERIVED ceiling and the tool is HIGH risk.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import secrets
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
__all__ = ["DeferralManager", "DeferralRecord"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _generate_deferral_id() -> str:
|
|
27
|
+
return f"defer_{secrets.token_hex(8)}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DeferralRecord:
|
|
32
|
+
"""A suspended authorization decision."""
|
|
33
|
+
|
|
34
|
+
deferral_id: str = field(default_factory=_generate_deferral_id)
|
|
35
|
+
tool_name: str = ""
|
|
36
|
+
user_id: str = ""
|
|
37
|
+
role: str = ""
|
|
38
|
+
reason: str = ""
|
|
39
|
+
trigger: str = ""
|
|
40
|
+
created_at: float = field(default_factory=time.time)
|
|
41
|
+
timeout_seconds: int = 60
|
|
42
|
+
resolution: str | None = None # "approved", "denied", "timeout"
|
|
43
|
+
resolved_at: float | None = None
|
|
44
|
+
resolved_by: str | None = None
|
|
45
|
+
parameters: dict[str, Any] | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_resolved(self) -> bool:
|
|
49
|
+
return self.resolution is not None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_expired(self) -> bool:
|
|
53
|
+
if self.is_resolved:
|
|
54
|
+
return False
|
|
55
|
+
return time.time() > (self.created_at + self.timeout_seconds)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DeferralManager:
|
|
59
|
+
"""Tracks deferred decisions and their resolution.
|
|
60
|
+
|
|
61
|
+
The manager evaluates trigger conditions and creates deferral
|
|
62
|
+
records. Resolution is handled externally (human review, additional
|
|
63
|
+
context, or timeout).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self) -> None:
|
|
67
|
+
self._deferrals: dict[str, DeferralRecord] = {}
|
|
68
|
+
self._session_call_counts: dict[str, int] = {}
|
|
69
|
+
|
|
70
|
+
def check_first_call_high_risk(
|
|
71
|
+
self,
|
|
72
|
+
session_id: str,
|
|
73
|
+
tool_name: str,
|
|
74
|
+
risk_level: str,
|
|
75
|
+
) -> DeferralRecord | None:
|
|
76
|
+
"""Trigger: first tool call in session is HIGH/CRITICAL risk.
|
|
77
|
+
|
|
78
|
+
Returns a DeferralRecord if triggered, None otherwise.
|
|
79
|
+
"""
|
|
80
|
+
count = self._session_call_counts.get(session_id, 0)
|
|
81
|
+
if count > 0:
|
|
82
|
+
return None
|
|
83
|
+
if risk_level not in ("high", "critical"):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
record = DeferralRecord(
|
|
87
|
+
tool_name=tool_name,
|
|
88
|
+
reason=(
|
|
89
|
+
f"First tool call in session is {risk_level} risk. "
|
|
90
|
+
f"Deferring for additional context."
|
|
91
|
+
),
|
|
92
|
+
trigger="first_call_high_risk",
|
|
93
|
+
)
|
|
94
|
+
self._deferrals[record.deferral_id] = record
|
|
95
|
+
return record
|
|
96
|
+
|
|
97
|
+
def check_scan_plus_tool(
|
|
98
|
+
self,
|
|
99
|
+
session_id: str,
|
|
100
|
+
tool_name: str,
|
|
101
|
+
scan_signals_this_turn: list[Any],
|
|
102
|
+
) -> DeferralRecord | None:
|
|
103
|
+
"""Trigger: prompt scanner fired AND tool call attempted.
|
|
104
|
+
|
|
105
|
+
Returns a DeferralRecord if triggered, None otherwise.
|
|
106
|
+
"""
|
|
107
|
+
if not scan_signals_this_turn:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
signal_types = [
|
|
111
|
+
s.signal_type if hasattr(s, "signal_type") else str(s)
|
|
112
|
+
for s in scan_signals_this_turn
|
|
113
|
+
]
|
|
114
|
+
record = DeferralRecord(
|
|
115
|
+
tool_name=tool_name,
|
|
116
|
+
reason=(
|
|
117
|
+
f"Prompt scan detected suspicious input "
|
|
118
|
+
f"({', '.join(signal_types)}) and tool call attempted. "
|
|
119
|
+
f"Deferring pending review."
|
|
120
|
+
),
|
|
121
|
+
trigger="scan_plus_tool",
|
|
122
|
+
)
|
|
123
|
+
self._deferrals[record.deferral_id] = record
|
|
124
|
+
return record
|
|
125
|
+
|
|
126
|
+
def check_trust_below_threshold(
|
|
127
|
+
self,
|
|
128
|
+
session_id: str,
|
|
129
|
+
tool_name: str,
|
|
130
|
+
risk_level: str,
|
|
131
|
+
trust_ceiling: str,
|
|
132
|
+
) -> DeferralRecord | None:
|
|
133
|
+
"""Trigger: trust degraded below DERIVED and tool is HIGH risk.
|
|
134
|
+
|
|
135
|
+
Returns a DeferralRecord if triggered, None otherwise.
|
|
136
|
+
"""
|
|
137
|
+
if risk_level not in ("high", "critical"):
|
|
138
|
+
return None
|
|
139
|
+
if trust_ceiling not in ("untrusted",):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
record = DeferralRecord(
|
|
143
|
+
tool_name=tool_name,
|
|
144
|
+
reason=(
|
|
145
|
+
f"Session trust at '{trust_ceiling}' — too low for "
|
|
146
|
+
f"{risk_level} risk tool '{tool_name}'. "
|
|
147
|
+
f"Deferring pending human review."
|
|
148
|
+
),
|
|
149
|
+
trigger="trust_below_threshold",
|
|
150
|
+
)
|
|
151
|
+
self._deferrals[record.deferral_id] = record
|
|
152
|
+
return record
|
|
153
|
+
|
|
154
|
+
def record_call(self, session_id: str) -> None:
|
|
155
|
+
"""Record that a tool call was attempted in the session."""
|
|
156
|
+
self._session_call_counts[session_id] = (
|
|
157
|
+
self._session_call_counts.get(session_id, 0) + 1
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def get_call_count(self, session_id: str) -> int:
|
|
161
|
+
"""Get the number of tool calls recorded for a session."""
|
|
162
|
+
return self._session_call_counts.get(session_id, 0)
|
|
163
|
+
|
|
164
|
+
def resolve(
|
|
165
|
+
self,
|
|
166
|
+
deferral_id: str,
|
|
167
|
+
resolution: str,
|
|
168
|
+
resolved_by: str = "",
|
|
169
|
+
) -> DeferralRecord | None:
|
|
170
|
+
"""Resolve a deferred decision.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
deferral_id: The deferral to resolve.
|
|
174
|
+
resolution: "approved", "denied", or "timeout".
|
|
175
|
+
resolved_by: Who resolved (user_id or "timeout").
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The resolved DeferralRecord, or None if not found.
|
|
179
|
+
"""
|
|
180
|
+
record = self._deferrals.get(deferral_id)
|
|
181
|
+
if record is None:
|
|
182
|
+
return None
|
|
183
|
+
record.resolution = resolution
|
|
184
|
+
record.resolved_at = time.time()
|
|
185
|
+
record.resolved_by = resolved_by
|
|
186
|
+
return record
|
|
187
|
+
|
|
188
|
+
def check_timeouts(self, timeout_action: str = "deny") -> list[DeferralRecord]:
|
|
189
|
+
"""Check for expired deferrals and resolve them.
|
|
190
|
+
|
|
191
|
+
Returns list of newly timed-out records.
|
|
192
|
+
"""
|
|
193
|
+
timed_out: list[DeferralRecord] = []
|
|
194
|
+
for record in self._deferrals.values():
|
|
195
|
+
if not record.is_resolved and record.is_expired:
|
|
196
|
+
record.resolution = timeout_action
|
|
197
|
+
record.resolved_at = time.time()
|
|
198
|
+
record.resolved_by = "timeout"
|
|
199
|
+
timed_out.append(record)
|
|
200
|
+
return timed_out
|
|
201
|
+
|
|
202
|
+
def get(self, deferral_id: str) -> DeferralRecord | None:
|
|
203
|
+
"""Get a deferral record by ID."""
|
|
204
|
+
return self._deferrals.get(deferral_id)
|
|
205
|
+
|
|
206
|
+
def reset_session(self, session_id: str) -> None:
|
|
207
|
+
"""Clear call count tracking for a session."""
|
|
208
|
+
self._session_call_counts.pop(session_id, None)
|
|
209
|
+
|
|
210
|
+
def __len__(self) -> int:
|
|
211
|
+
return len(self._deferrals)
|