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.
Files changed (86) hide show
  1. agentlock-1.2.0/CHANGELOG.md +135 -0
  2. {agentlock-1.1.2 → agentlock-1.2.0}/PKG-INFO +53 -4
  3. {agentlock-1.1.2 → agentlock-1.2.0}/README.md +52 -3
  4. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/__init__.py +58 -1
  5. agentlock-1.2.0/agentlock/defer.py +211 -0
  6. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/exceptions.py +64 -0
  7. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/gate.py +422 -8
  8. agentlock-1.2.0/agentlock/hardening.py +451 -0
  9. agentlock-1.2.0/agentlock/modify.py +242 -0
  10. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/redaction.py +10 -1
  11. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/schema.py +73 -1
  12. agentlock-1.2.0/agentlock/signals/__init__.py +30 -0
  13. agentlock-1.2.0/agentlock/signals/combos.py +205 -0
  14. agentlock-1.2.0/agentlock/signals/echo.py +277 -0
  15. agentlock-1.2.0/agentlock/signals/prompt_scan.py +383 -0
  16. agentlock-1.2.0/agentlock/signals/velocity.py +221 -0
  17. agentlock-1.2.0/agentlock/stepup.py +255 -0
  18. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/types.py +11 -0
  19. agentlock-1.2.0/docs/benchmark.md +385 -0
  20. {agentlock-1.1.2 → agentlock-1.2.0}/pyproject.toml +1 -1
  21. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_backward_compat.py +4 -4
  22. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_cli.py +1 -1
  23. agentlock-1.2.0/tests/test_combos.py +239 -0
  24. agentlock-1.2.0/tests/test_compound.py +150 -0
  25. agentlock-1.2.0/tests/test_defer.py +248 -0
  26. agentlock-1.2.0/tests/test_echo.py +249 -0
  27. agentlock-1.2.0/tests/test_gate_v12.py +330 -0
  28. agentlock-1.2.0/tests/test_hardening.py +635 -0
  29. agentlock-1.2.0/tests/test_modify.py +337 -0
  30. agentlock-1.2.0/tests/test_prompt_scan.py +269 -0
  31. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_redaction.py +53 -0
  32. agentlock-1.2.0/tests/test_stepup.py +326 -0
  33. agentlock-1.2.0/tests/test_velocity.py +166 -0
  34. agentlock-1.1.2/CHANGELOG.md +0 -62
  35. {agentlock-1.1.2 → agentlock-1.2.0}/.github/workflows/ci.yml +0 -0
  36. {agentlock-1.1.2 → agentlock-1.2.0}/.gitignore +0 -0
  37. {agentlock-1.1.2 → agentlock-1.2.0}/CONTRIBUTING.md +0 -0
  38. {agentlock-1.1.2 → agentlock-1.2.0}/LICENSE +0 -0
  39. {agentlock-1.1.2 → agentlock-1.2.0}/NOTICE +0 -0
  40. {agentlock-1.1.2 → agentlock-1.2.0}/SECURITY.md +0 -0
  41. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/audit.py +0 -0
  42. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/auth_providers/__init__.py +0 -0
  43. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/cli.py +0 -0
  44. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/context.py +0 -0
  45. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/decorators.py +0 -0
  46. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/__init__.py +0 -0
  47. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/autogen.py +0 -0
  48. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/crewai.py +0 -0
  49. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/fastapi.py +0 -0
  50. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/flask.py +0 -0
  51. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/langchain.py +0 -0
  52. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/integrations/mcp.py +0 -0
  53. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/memory_gate.py +0 -0
  54. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/policy.py +0 -0
  55. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/py.typed +0 -0
  56. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/rate_limit.py +0 -0
  57. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/session.py +0 -0
  58. {agentlock-1.1.2 → agentlock-1.2.0}/agentlock/token.py +0 -0
  59. {agentlock-1.1.2 → agentlock-1.2.0}/docs/integrations.md +0 -0
  60. {agentlock-1.1.2 → agentlock-1.2.0}/docs/quickstart.md +0 -0
  61. {agentlock-1.1.2 → agentlock-1.2.0}/docs/specification.md +0 -0
  62. {agentlock-1.1.2 → agentlock-1.2.0}/examples/data_redaction.py +0 -0
  63. {agentlock-1.1.2 → agentlock-1.2.0}/examples/decorator_example.py +0 -0
  64. {agentlock-1.1.2 → agentlock-1.2.0}/examples/fastapi_app.py +0 -0
  65. {agentlock-1.1.2 → agentlock-1.2.0}/examples/multi_role.py +0 -0
  66. {agentlock-1.1.2 → agentlock-1.2.0}/examples/quickstart.py +0 -0
  67. {agentlock-1.1.2 → agentlock-1.2.0}/examples/rate_limiting.py +0 -0
  68. {agentlock-1.1.2 → agentlock-1.2.0}/schema/agentlock-v1.0.json +0 -0
  69. {agentlock-1.1.2 → agentlock-1.2.0}/tests/__init__.py +0 -0
  70. {agentlock-1.1.2 → agentlock-1.2.0}/tests/conftest.py +0 -0
  71. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_audit.py +0 -0
  72. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_audit_v11.py +0 -0
  73. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_context.py +0 -0
  74. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_decorators.py +0 -0
  75. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_filter_pipeline.py +0 -0
  76. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_gate.py +0 -0
  77. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_gate_v11.py +0 -0
  78. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_memory_gate.py +0 -0
  79. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_pii_defense.py +0 -0
  80. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_policy.py +0 -0
  81. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_policy_v11.py +0 -0
  82. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_rate_limit.py +0 -0
  83. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_schema.py +0 -0
  84. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_schema_v11.py +0 -0
  85. {agentlock-1.1.2 → agentlock-1.2.0}/tests/test_session.py +0 -0
  86. {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.1.2
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: **187 multi-turn adversarial attack tests** across 35 categories, tested against 6 frontier AI models.
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** | Multi-agent permissions, cross-agent identity delegation |
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: **187 multi-turn adversarial attack tests** across 35 categories, tested against 6 frontier AI models.
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** | Multi-agent permissions, cross-agent identity delegation |
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.1.2"
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)