agentlock 1.2.0__tar.gz → 1.2.1__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 (93) hide show
  1. {agentlock-1.2.0 → agentlock-1.2.1}/CHANGELOG.md +39 -0
  2. {agentlock-1.2.0 → agentlock-1.2.1}/PKG-INFO +37 -2
  3. {agentlock-1.2.0 → agentlock-1.2.1}/README.md +33 -1
  4. agentlock-1.2.1/SECURITY.md +139 -0
  5. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/__init__.py +34 -22
  6. agentlock-1.2.1/agentlock/chain.py +163 -0
  7. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/context.py +24 -0
  8. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/defer.py +73 -2
  9. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/gate.py +290 -11
  10. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/hardening.py +2 -2
  11. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/modify.py +8 -2
  12. agentlock-1.2.1/agentlock/receipts.py +211 -0
  13. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/schema.py +1 -0
  14. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/combos.py +3 -2
  15. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/echo.py +0 -2
  16. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/prompt_scan.py +1 -2
  17. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/velocity.py +1 -2
  18. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/stepup.py +1 -1
  19. {agentlock-1.2.0 → agentlock-1.2.1}/docs/specification.md +94 -2
  20. {agentlock-1.2.0 → agentlock-1.2.1}/examples/fastapi_app.py +3 -2
  21. {agentlock-1.2.0 → agentlock-1.2.1}/examples/rate_limiting.py +1 -1
  22. {agentlock-1.2.0 → agentlock-1.2.1}/pyproject.toml +6 -1
  23. agentlock-1.2.1/schema/agentlock-v1.2.json +744 -0
  24. agentlock-1.2.1/tests/test_benchmark_gaps.py +543 -0
  25. agentlock-1.2.1/tests/test_benchmark_gaps_v2.py +212 -0
  26. agentlock-1.2.1/tests/test_chain.py +420 -0
  27. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_cli.py +1 -1
  28. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_combos.py +0 -2
  29. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_compound.py +0 -2
  30. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_defer.py +2 -4
  31. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_echo.py +0 -3
  32. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_gate_v12.py +0 -3
  33. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_hardening.py +21 -17
  34. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_modify.py +0 -2
  35. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_prompt_scan.py +27 -14
  36. agentlock-1.2.1/tests/test_receipts.py +392 -0
  37. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_stepup.py +5 -7
  38. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_velocity.py +0 -2
  39. agentlock-1.2.0/SECURITY.md +0 -89
  40. {agentlock-1.2.0 → agentlock-1.2.1}/.github/workflows/ci.yml +0 -0
  41. {agentlock-1.2.0 → agentlock-1.2.1}/.gitignore +0 -0
  42. {agentlock-1.2.0 → agentlock-1.2.1}/CONTRIBUTING.md +0 -0
  43. {agentlock-1.2.0 → agentlock-1.2.1}/LICENSE +0 -0
  44. {agentlock-1.2.0 → agentlock-1.2.1}/NOTICE +0 -0
  45. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/audit.py +0 -0
  46. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/auth_providers/__init__.py +0 -0
  47. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/cli.py +0 -0
  48. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/decorators.py +0 -0
  49. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/exceptions.py +0 -0
  50. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/__init__.py +0 -0
  51. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/autogen.py +0 -0
  52. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/crewai.py +0 -0
  53. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/fastapi.py +0 -0
  54. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/flask.py +0 -0
  55. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/langchain.py +0 -0
  56. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/mcp.py +0 -0
  57. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/memory_gate.py +0 -0
  58. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/policy.py +0 -0
  59. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/py.typed +0 -0
  60. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/rate_limit.py +0 -0
  61. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/redaction.py +0 -0
  62. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/session.py +0 -0
  63. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/__init__.py +0 -0
  64. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/token.py +0 -0
  65. {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/types.py +0 -0
  66. {agentlock-1.2.0 → agentlock-1.2.1}/docs/benchmark.md +0 -0
  67. {agentlock-1.2.0 → agentlock-1.2.1}/docs/integrations.md +0 -0
  68. {agentlock-1.2.0 → agentlock-1.2.1}/docs/quickstart.md +0 -0
  69. {agentlock-1.2.0 → agentlock-1.2.1}/examples/data_redaction.py +0 -0
  70. {agentlock-1.2.0 → agentlock-1.2.1}/examples/decorator_example.py +0 -0
  71. {agentlock-1.2.0 → agentlock-1.2.1}/examples/multi_role.py +0 -0
  72. {agentlock-1.2.0 → agentlock-1.2.1}/examples/quickstart.py +0 -0
  73. {agentlock-1.2.0 → agentlock-1.2.1}/schema/agentlock-v1.0.json +0 -0
  74. {agentlock-1.2.0 → agentlock-1.2.1}/tests/__init__.py +0 -0
  75. {agentlock-1.2.0 → agentlock-1.2.1}/tests/conftest.py +0 -0
  76. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_audit.py +0 -0
  77. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_audit_v11.py +0 -0
  78. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_backward_compat.py +0 -0
  79. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_context.py +0 -0
  80. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_decorators.py +0 -0
  81. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_filter_pipeline.py +0 -0
  82. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_gate.py +0 -0
  83. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_gate_v11.py +0 -0
  84. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_memory_gate.py +0 -0
  85. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_pii_defense.py +0 -0
  86. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_policy.py +0 -0
  87. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_policy_v11.py +0 -0
  88. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_rate_limit.py +0 -0
  89. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_redaction.py +0 -0
  90. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_schema.py +0 -0
  91. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_schema_v11.py +0 -0
  92. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_session.py +0 -0
  93. {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_token.py +0 -0
@@ -5,6 +5,45 @@ All notable changes to AgentLock will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.1] - 2026-04-06
9
+
10
+ ### Added
11
+
12
+ - **Ed25519 signed receipts (AARM R5)** -- Every authorization decision can produce a cryptographically signed receipt verifiable offline. `ReceiptSigner` supports Ed25519 (via PyNaCl) with HMAC-SHA256 fallback for environments without asymmetric key infrastructure. `ReceiptVerifier` detects tampered receipts. Receipts include decision, tool name, user identity, parameters hash, and policy version hash. Optional dependency: `pip install agentlock[crypto]`.
13
+ - **Hash-chained tamper-evident context (AARM R2)** -- `ContextChain` creates an append-only hash chain of context entries. Each entry links to the previous entry's hash, forming a tamper-evident log. `verify_chain()` detects modification of any historical entry. `ContextProvenance` gains a `previous_hash` field. `ContextTracker.verify_context_chain()` validates chain integrity per session.
14
+ - **`first_call_any_risk` DEFER trigger** -- Defers the first tool call in a session regardless of risk level. Configured via `DeferPolicyConfig(first_call_any_risk=True)`. Unlike `first_call_high_risk`, this catches MEDIUM and LOW risk tools used as attack footholds.
15
+ - **`deny_on_block` whitelist escalation** -- When a `whitelist_path` transformation blocks a parameter (replacing it with `[BLOCKED: ...]`), the gate escalates from MODIFY to DENY. The tool does not execute. `ModifyResult` gains a `blocked_fields` list.
16
+ - **Sibling deferral** -- When one tool call is DEFERRED in a turn, subsequent tool calls in the same turn (within a 5-second window) are automatically deferred. Prevents attackers from falling through to lower-friction tools after a DEFER fires.
17
+ - **Prompt scan carry-forward** -- When a `prompt_scan` signal fires in a session, ALL subsequent tool calls are deferred regardless of whether the tool has its own `defer_policy`. Previously, tools without `defer_policy` (like `lookup_order`) bypassed scan-triggered deferral.
18
+ - **`enforce_all_at_critical` hardening** -- When `HardeningConfig(enforce_all_at_critical=True)`, the gate blocks ALL tool calls at critical hardening severity (risk score >= 10), regardless of tool risk level. Previously, `enforce_at_critical` only blocked HIGH/CRITICAL risk tools.
19
+ - **3 new `lookup_order` combo pairs** -- `(lookup_order, query_database)` weight 4, `(lookup_order, check_balance)` weight 3, `(lookup_order, search_contacts)` weight 3. Detects reconnaissance-to-data-access patterns.
20
+ - New modules: `agentlock/receipts.py`, `agentlock/chain.py`
21
+ - New exports: `SignedReceipt`, `ReceiptSigner`, `ReceiptVerifier`, `ChainedContextEntry`, `ContextChain`, `GENESIS_HASH`
22
+ - Optional dependency group: `agentlock[crypto]` for PyNaCl >= 1.5.0
23
+ - 102 new tests (847 total, 0 failures)
24
+
25
+ ### Changed
26
+
27
+ - `AuthResult` gains `receipt: SignedReceipt | None` field
28
+ - `AuthorizationGate.__init__()` accepts optional `receipt_signer` parameter
29
+ - `DeferPolicyConfig` gains `first_call_any_risk: bool` field (default False)
30
+ - `DeferralManager` gains `check_first_call_any_risk()`, `check_sibling_deferral()`, `record_deferral()` methods
31
+ - `ModifyResult` gains `blocked_fields: list[str]` field
32
+ - `ContextProvenance` gains `previous_hash: str` field
33
+ - `ContextState` gains `context_chain: ContextChain` field
34
+ - `ContextTracker` gains `verify_context_chain()` method
35
+ - Package version updated to 1.2.1
36
+
37
+ ### Backward Compatibility
38
+
39
+ - All 778 v1.2.0 tests pass without modification
40
+ - `receipt` field defaults to None when no signer is configured
41
+ - `first_call_any_risk` defaults to False
42
+ - `enforce_all_at_critical` defaults to False
43
+ - `blocked_fields` defaults to empty list
44
+ - `previous_hash` defaults to empty string
45
+ - Hash chain is populated transparently; existing `record_write()` callers require no changes
46
+
8
47
  ## [1.2.0] - 2026-03-30
9
48
 
10
49
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentlock
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -35,11 +35,14 @@ Requires-Dist: flask>=2.0; extra == 'all'
35
35
  Requires-Dist: langchain-core>=0.2; extra == 'all'
36
36
  Requires-Dist: mcp>=1.0; extra == 'all'
37
37
  Requires-Dist: pyautogen>=0.2; extra == 'all'
38
+ Requires-Dist: pynacl>=1.5.0; extra == 'all'
38
39
  Requires-Dist: python-jose[cryptography]>=3.3; extra == 'all'
39
40
  Provides-Extra: autogen
40
41
  Requires-Dist: pyautogen>=0.2; extra == 'autogen'
41
42
  Provides-Extra: crewai
42
43
  Requires-Dist: crewai>=0.30; extra == 'crewai'
44
+ Provides-Extra: crypto
45
+ Requires-Dist: pynacl>=1.5.0; extra == 'crypto'
43
46
  Provides-Extra: dev
44
47
  Requires-Dist: mypy>=1.10; extra == 'dev'
45
48
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
@@ -261,6 +264,7 @@ pip install agentlock[autogen] # AutoGen
261
264
  pip install agentlock[mcp] # Model Context Protocol
262
265
  pip install agentlock[fastapi] # FastAPI
263
266
  pip install agentlock[flask] # Flask
267
+ pip install agentlock[crypto] # Ed25519 signed receipts
264
268
  pip install agentlock[all] # Everything
265
269
  ```
266
270
 
@@ -424,6 +428,37 @@ output = gate.execute("query_database", db_func, token=result.token,
424
428
 
425
429
  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
430
 
431
+ ### Signed Receipts (AARM R5)
432
+
433
+ Every authorization decision can produce a cryptographically signed receipt, verifiable offline without access to the gate. Tampered receipts fail signature verification.
434
+
435
+ ```python
436
+ from agentlock import AuthorizationGate, ReceiptSigner, ReceiptVerifier
437
+
438
+ signer = ReceiptSigner(signing_method="ed25519")
439
+ gate = AuthorizationGate(receipt_signer=signer)
440
+
441
+ result = gate.authorize("query_database", user_id="alice", role="admin")
442
+ # result.receipt is a SignedReceipt with Ed25519 signature
443
+
444
+ verifier = ReceiptVerifier(signing_method="ed25519", verify_key=signer.verify_key_bytes)
445
+ assert verifier.verify(result.receipt) # True
446
+ ```
447
+
448
+ HMAC-SHA256 is available as a fallback when PyNaCl is not installed. Install Ed25519 support with `pip install agentlock[crypto]`.
449
+
450
+ ### Hash-Chained Context (AARM R2)
451
+
452
+ Context entries form a tamper-evident append-only chain. Each entry includes the hash of the previous entry. Modifying any entry invalidates all subsequent entries.
453
+
454
+ ```python
455
+ gate.notify_context_write(session_id, source=ContextSource.TOOL_OUTPUT,
456
+ content_hash="abc123...")
457
+
458
+ valid, broken_at = gate.context_tracker.verify_context_chain(session_id)
459
+ # (True, None) if intact, (False, index) if tampered
460
+ ```
461
+
427
462
  ## Standards Alignment
428
463
 
429
464
  | Standard | Coverage |
@@ -441,7 +476,7 @@ The tool still executes. The admin still gets the answer. But PII never enters t
441
476
  |---------|-------|
442
477
  | **v1.0** | Core schema, tool permissions, enforcement architecture |
443
478
  | **v1.1** | Memory/context permissions, trust degradation, provenance tracking |
444
- | **v1.2** | Adaptive hardening, MODIFY/DEFER/STEP_UP decisions, multi-signal detection (745 tests) |
479
+ | **v1.2** | Adaptive hardening, MODIFY/DEFER/STEP_UP decisions, signed receipts, hash-chained context (847 tests) |
445
480
  | **v1.3** | Output destination control, data flow policies |
446
481
  | **v2.0** | Execution scope, behavioral policy, anomaly detection, compliance templates |
447
482
 
@@ -202,6 +202,7 @@ pip install agentlock[autogen] # AutoGen
202
202
  pip install agentlock[mcp] # Model Context Protocol
203
203
  pip install agentlock[fastapi] # FastAPI
204
204
  pip install agentlock[flask] # Flask
205
+ pip install agentlock[crypto] # Ed25519 signed receipts
205
206
  pip install agentlock[all] # Everything
206
207
  ```
207
208
 
@@ -365,6 +366,37 @@ output = gate.execute("query_database", db_func, token=result.token,
365
366
 
366
367
  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
368
 
369
+ ### Signed Receipts (AARM R5)
370
+
371
+ Every authorization decision can produce a cryptographically signed receipt, verifiable offline without access to the gate. Tampered receipts fail signature verification.
372
+
373
+ ```python
374
+ from agentlock import AuthorizationGate, ReceiptSigner, ReceiptVerifier
375
+
376
+ signer = ReceiptSigner(signing_method="ed25519")
377
+ gate = AuthorizationGate(receipt_signer=signer)
378
+
379
+ result = gate.authorize("query_database", user_id="alice", role="admin")
380
+ # result.receipt is a SignedReceipt with Ed25519 signature
381
+
382
+ verifier = ReceiptVerifier(signing_method="ed25519", verify_key=signer.verify_key_bytes)
383
+ assert verifier.verify(result.receipt) # True
384
+ ```
385
+
386
+ HMAC-SHA256 is available as a fallback when PyNaCl is not installed. Install Ed25519 support with `pip install agentlock[crypto]`.
387
+
388
+ ### Hash-Chained Context (AARM R2)
389
+
390
+ Context entries form a tamper-evident append-only chain. Each entry includes the hash of the previous entry. Modifying any entry invalidates all subsequent entries.
391
+
392
+ ```python
393
+ gate.notify_context_write(session_id, source=ContextSource.TOOL_OUTPUT,
394
+ content_hash="abc123...")
395
+
396
+ valid, broken_at = gate.context_tracker.verify_context_chain(session_id)
397
+ # (True, None) if intact, (False, index) if tampered
398
+ ```
399
+
368
400
  ## Standards Alignment
369
401
 
370
402
  | Standard | Coverage |
@@ -382,7 +414,7 @@ The tool still executes. The admin still gets the answer. But PII never enters t
382
414
  |---------|-------|
383
415
  | **v1.0** | Core schema, tool permissions, enforcement architecture |
384
416
  | **v1.1** | Memory/context permissions, trust degradation, provenance tracking |
385
- | **v1.2** | Adaptive hardening, MODIFY/DEFER/STEP_UP decisions, multi-signal detection (745 tests) |
417
+ | **v1.2** | Adaptive hardening, MODIFY/DEFER/STEP_UP decisions, signed receipts, hash-chained context (847 tests) |
386
418
  | **v1.3** | Output destination control, data flow policies |
387
419
  | **v2.0** | Execution scope, behavioral policy, anomaly detection, compliance templates |
388
420
 
@@ -0,0 +1,139 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|--------------------|
7
+ | 1.2.x | Yes |
8
+ | 1.1.x | Yes |
9
+ | 1.0.x | Yes |
10
+
11
+ ## Reporting a Vulnerability
12
+
13
+ AgentLock is a security-critical library. We take vulnerability reports seriously and aim to respond within 48 hours.
14
+
15
+ ### How to Report
16
+
17
+ **Do NOT open a public GitHub issue for security vulnerabilities.**
18
+
19
+ Instead, report vulnerabilities via one of these channels:
20
+
21
+ 1. **Email:** security@agentlock.dev
22
+ 2. **GitHub Security Advisories:** Use the "Report a vulnerability" button on the [Security tab](https://github.com/webpro255/agentlock/security/advisories/new)
23
+
24
+ ### What to Include
25
+
26
+ - Description of the vulnerability
27
+ - Steps to reproduce
28
+ - Affected versions
29
+ - Potential impact assessment
30
+ - Suggested fix (if any)
31
+
32
+ ### Response Timeline
33
+
34
+ | Step | Timeline |
35
+ |------|----------|
36
+ | Acknowledgment | Within 48 hours |
37
+ | Initial assessment | Within 5 business days |
38
+ | Fix development | Depends on severity |
39
+ | Public disclosure | After fix is released |
40
+
41
+ ### Severity Classification
42
+
43
+ We use CVSS v3.1 for severity scoring:
44
+
45
+ - **Critical (9.0-10.0):** Fix and release within 48 hours
46
+ - **High (7.0-8.9):** Fix and release within 7 days
47
+ - **Medium (4.0-6.9):** Fix in next scheduled release
48
+ - **Low (0.1-3.9):** Fix when resources allow
49
+
50
+ ### Responsible Disclosure
51
+
52
+ We follow coordinated disclosure:
53
+
54
+ 1. Reporter contacts us privately
55
+ 2. We acknowledge and assess
56
+ 3. We develop and test a fix
57
+ 4. We release the fix and publish an advisory
58
+ 5. Reporter may publish details 30 days after fix release
59
+
60
+ ### Scope
61
+
62
+ The following are in scope for security reports:
63
+
64
+ - Authorization bypass in the gate or policy engine
65
+ - Token replay or forgery
66
+ - Receipt signature forgery or verification bypass
67
+ - Context chain integrity bypass (hash chain tampering)
68
+ - DEFER/STEP_UP bypass (tool executing when it should be deferred)
69
+ - Session hijacking or fixation
70
+ - Rate limit bypass
71
+ - Redaction bypass (prohibited data leaking through)
72
+ - Audit log tampering or omission
73
+ - Dependency vulnerabilities affecting core functionality
74
+
75
+ ### Out of Scope
76
+
77
+ - Vulnerabilities in optional framework integrations caused by the framework itself
78
+ - Issues requiring physical access to the server
79
+ - Social engineering attacks against project maintainers
80
+ - Denial of service via resource exhaustion (unless trivially exploitable)
81
+
82
+ ### Recognition
83
+
84
+ We maintain a security acknowledgments list. Reporters who follow responsible disclosure will be credited (with permission) in our CHANGELOG and security advisories.
85
+
86
+ ## Security Design Principles
87
+
88
+ AgentLock is built on these security principles:
89
+
90
+ - **Deny by default** -- no permissions = no access
91
+ - **Infrastructure enforcement** -- agent cannot bypass the gate
92
+ - **Out-of-band authentication** -- credentials never touch the conversation
93
+ - **Single-use tokens** -- cannot be replayed
94
+ - **Mandatory audit** -- every call logged, no exceptions
95
+ - **Signed receipts** -- authorization decisions are cryptographically signed (Ed25519 or HMAC-SHA256) and verifiable offline
96
+ - **Tamper-evident context** -- hash-chained context entries detect modification of historical records
97
+ - **Monotonic trust degradation** -- session trust only goes down, never up
98
+
99
+ ## Security Testing
100
+
101
+ AgentLock is tested against adversarial attack vectors across 35 categories including prompt injection, social engineering, data exfiltration, privilege escalation, tool chain attacks, memory poisoning, crisis exploitation, and refusal exhaustion.
102
+
103
+ v1.2.1 results (222 vectors, scored by AgentShield):
104
+
105
+ | Metric | Score |
106
+ |--------|-------|
107
+ | Overall | 88.7/B |
108
+ | Injection pass rate | 93%+ |
109
+ | PII protection | 100/A |
110
+ | Jailbreak resistance | 100% |
111
+ | Tool abuse prevention | 80%+ |
112
+
113
+ Remaining failures are concentrated in categories where the model's helpfulness is exploited through plausible narratives with zero injection language. These are model-layer behaviors that middleware alone cannot fully address.
114
+
115
+ ## Defense Layers (v1.2.1)
116
+
117
+ | Layer | Component | What It Does |
118
+ |-------|-----------|-------------|
119
+ | Pre-LLM | Prompt scanner | Detects injection, authority claims, encoding, impersonation |
120
+ | Gate | Policy engine | Role, scope, rate limit, data classification checks |
121
+ | Gate | Injection filter | Blocks reconnaissance, command injection, social engineering patterns |
122
+ | Gate | PII filter | Blocks queries that would return data above caller clearance |
123
+ | Gate | DEFER engine | Suspends ambiguous calls pending review |
124
+ | Gate | STEP_UP engine | Requires human approval based on session risk |
125
+ | Gate | MODIFY engine | Transforms parameters and outputs (PII redaction, domain restriction) |
126
+ | Gate | Hardening engine | Monotonic risk scoring with targeted defensive instructions |
127
+ | Post-execution | Redaction engine | Pattern-based PII removal from tool outputs |
128
+ | Forensic | Signed receipts | Ed25519/HMAC-SHA256 signed authorization records |
129
+ | Forensic | Context chain | Hash-chained tamper-evident context log |
130
+
131
+ ## Signal Detectors
132
+
133
+ | Detector | Signals |
134
+ |----------|---------|
135
+ | Velocity | Rapid calls, topic escalation, burst patterns |
136
+ | Combo | Suspicious tool pairs and sequences (16 pairs, 5 sequences) |
137
+ | Echo | Response echoing, tool disclosure, credential patterns |
138
+ | Prompt scan | 7 pattern categories with cross-turn repetition tracking |
139
+ | Compound | Bonus scoring when multiple signal types co-occur |
@@ -34,19 +34,19 @@ Copyright 2026 David Grice
34
34
  SPDX-License-Identifier: Apache-2.0
35
35
  """
36
36
 
37
- __version__ = "1.2.0"
37
+ __version__ = "1.2.1"
38
38
 
39
39
  from agentlock.audit import AuditLogger, AuditRecord, FileAuditBackend, InMemoryAuditBackend
40
+ from agentlock.chain import GENESIS_HASH, ChainedContextEntry, ContextChain
40
41
  from agentlock.context import ContextProvenance, ContextState, ContextTracker
41
42
  from agentlock.decorators import agentlock
43
+ from agentlock.defer import DeferralManager, DeferralRecord
42
44
  from agentlock.exceptions import (
43
45
  AgentLockError,
44
- DeferredError,
45
- ModifyAppliedError,
46
- StepUpRequiredError,
47
46
  ApprovalRequiredError,
48
47
  AuthenticationRequiredError,
49
48
  ConfigurationError,
49
+ DeferredError,
50
50
  DeniedError,
51
51
  InsufficientRoleError,
52
52
  MemoryConfirmationRequiredError,
@@ -54,10 +54,12 @@ from agentlock.exceptions import (
54
54
  MemoryReadDeniedError,
55
55
  MemoryRetentionExceededError,
56
56
  MemoryWriteDeniedError,
57
+ ModifyAppliedError,
57
58
  RateLimitedError,
58
59
  SchemaValidationError,
59
60
  ScopeViolationError,
60
61
  SessionExpiredError,
62
+ StepUpRequiredError,
61
63
  TokenError,
62
64
  TokenExpiredError,
63
65
  TokenInvalidError,
@@ -65,27 +67,15 @@ from agentlock.exceptions import (
65
67
  TrustDegradedError,
66
68
  UnattributedContextError,
67
69
  )
68
- from agentlock.defer import DeferralManager, DeferralRecord
69
70
  from agentlock.gate import AuthorizationGate, AuthResult
70
- from agentlock.stepup import StepUpManager, StepUpNotifier, StepUpRequest
71
- from agentlock.modify import ModifyEngine, ModifyResult
72
71
  from agentlock.hardening import (
73
72
  HardeningConfig,
74
73
  HardeningDirective,
75
74
  HardeningEngine,
76
75
  HardeningSignal,
77
76
  )
78
- from agentlock.signals import (
79
- ComboDetector,
80
- ComboSignal,
81
- EchoDetector,
82
- EchoSignal,
83
- PromptScanConfig,
84
- PromptScanner,
85
- VelocityDetector,
86
- VelocitySignal,
87
- )
88
77
  from agentlock.memory_gate import InMemoryMemoryStore, MemoryDecision, MemoryEntry, MemoryGate
78
+ from agentlock.modify import ModifyEngine, ModifyResult
89
79
  from agentlock.policy import (
90
80
  InjectionFilter,
91
81
  PiiFilter,
@@ -94,33 +84,44 @@ from agentlock.policy import (
94
84
  RequestContext,
95
85
  )
96
86
  from agentlock.rate_limit import RateLimiter
87
+ from agentlock.receipts import ReceiptSigner, ReceiptVerifier, SignedReceipt
97
88
  from agentlock.redaction import RedactionEngine, RedactionResult
98
89
  from agentlock.schema import (
99
90
  SCHEMA_VERSION,
100
91
  AgentLockPermissions,
101
- DeferPolicyConfig,
102
- ModifyPolicyConfig,
103
- StepUpPolicyConfig,
104
- TransformationConfig,
105
92
  AuditConfig,
106
93
  ContextPolicyConfig,
107
94
  DataPolicyConfig,
95
+ DeferPolicyConfig,
108
96
  DegradationTrigger,
109
97
  HumanApprovalConfig,
110
98
  MemoryPolicyConfig,
111
99
  MemoryRetentionConfig,
100
+ ModifyPolicyConfig,
112
101
  RateLimitConfig,
113
102
  ScopeConfig,
114
103
  SessionConfig,
115
104
  SourceAuthorityConfig,
105
+ StepUpPolicyConfig,
116
106
  ToolDefinition,
107
+ TransformationConfig,
117
108
  TrustDegradationConfig,
118
109
  )
119
110
  from agentlock.session import Session, SessionStore
111
+ from agentlock.signals import (
112
+ ComboDetector,
113
+ ComboSignal,
114
+ EchoDetector,
115
+ EchoSignal,
116
+ PromptScanConfig,
117
+ PromptScanner,
118
+ VelocityDetector,
119
+ VelocitySignal,
120
+ )
121
+ from agentlock.stepup import StepUpManager, StepUpNotifier, StepUpRequest
120
122
  from agentlock.token import ExecutionToken, TokenStore
121
123
  from agentlock.types import (
122
124
  ApprovalChannel,
123
- DecisionType,
124
125
  ApprovalThreshold,
125
126
  AuditLogLevel,
126
127
  AuthMethod,
@@ -128,6 +129,7 @@ from agentlock.types import (
128
129
  ContextSource,
129
130
  DataBoundary,
130
131
  DataClassification,
132
+ DecisionType,
131
133
  DegradationEffect,
132
134
  DenialReason,
133
135
  MemoryPersistence,
@@ -198,13 +200,23 @@ __all__ = [
198
200
  # DEFER (v1.2)
199
201
  "DeferralManager",
200
202
  "DeferralRecord",
203
+ "DeferredError",
201
204
  # STEP_UP (v1.2)
202
205
  "StepUpManager",
203
206
  "StepUpRequest",
204
207
  "StepUpNotifier",
208
+ "StepUpRequiredError",
205
209
  # MODIFY (v1.2)
206
210
  "ModifyEngine",
207
211
  "ModifyResult",
212
+ # Signed receipts (AARM R5)
213
+ "SignedReceipt",
214
+ "ReceiptSigner",
215
+ "ReceiptVerifier",
216
+ # Hash-chained context (AARM R2)
217
+ "ChainedContextEntry",
218
+ "ContextChain",
219
+ "GENESIS_HASH",
208
220
  # Enums
209
221
  "DecisionType",
210
222
  "RiskLevel",
@@ -0,0 +1,163 @@
1
+ """Hash-chained context entries (AARM R2).
2
+
3
+ Each context entry includes the hash of the previous entry, forming a
4
+ tamper-evident append-only chain. Modifying any entry invalidates all
5
+ subsequent entries.
6
+
7
+ Usage::
8
+
9
+ chain = ContextChain()
10
+ chain.append(source="user_message", authority="authoritative",
11
+ content_hash="abc123...")
12
+ chain.append(source="tool_output", authority="derived",
13
+ content_hash="def456...")
14
+
15
+ valid, broken_at = chain.verify_chain()
16
+ assert valid
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import secrets
23
+ import time
24
+ from dataclasses import dataclass, field
25
+ from typing import Any
26
+
27
+ __all__ = [
28
+ "ChainedContextEntry",
29
+ "ContextChain",
30
+ "GENESIS_HASH",
31
+ ]
32
+
33
+ # The chain starts from a well-known genesis hash (SHA-256 of empty string).
34
+ GENESIS_HASH = hashlib.sha256(b"").hexdigest()
35
+
36
+
37
+ def _generate_entry_id() -> str:
38
+ return f"cctx_{secrets.token_hex(8)}"
39
+
40
+
41
+ def _compute_entry_hash(
42
+ previous_hash: str,
43
+ content_hash: str,
44
+ source: str,
45
+ authority: str,
46
+ writer_id: str,
47
+ timestamp: float,
48
+ ) -> str:
49
+ """Compute the deterministic hash of a chain entry."""
50
+ payload = (
51
+ f"{previous_hash}\x00{content_hash}\x00"
52
+ f"{source}\x00{authority}\x00"
53
+ f"{writer_id}\x00{timestamp:.6f}"
54
+ )
55
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class ChainedContextEntry:
60
+ """A single entry in the hash chain."""
61
+
62
+ entry_id: str = field(default_factory=_generate_entry_id)
63
+ timestamp: float = field(default_factory=time.time)
64
+ source: str = "" # ContextSource value
65
+ authority: str = "" # ContextAuthority value
66
+ content_hash: str = "" # SHA-256 of actual content
67
+ previous_hash: str = "" # hash of the previous ChainedContextEntry
68
+ entry_hash: str = "" # SHA-256(previous_hash + content_hash + ...)
69
+ writer_id: str = ""
70
+ metadata: dict[str, Any] = field(default_factory=dict)
71
+
72
+
73
+ class ContextChain:
74
+ """Append-only hash chain of context entries.
75
+
76
+ Each appended entry is linked to the previous entry's ``entry_hash``,
77
+ forming a tamper-evident log. Verifying the chain detects any
78
+ modification to historical entries.
79
+ """
80
+
81
+ def __init__(self) -> None:
82
+ self._entries: list[ChainedContextEntry] = []
83
+
84
+ def append(
85
+ self,
86
+ source: str,
87
+ authority: str,
88
+ content_hash: str,
89
+ *,
90
+ writer_id: str = "",
91
+ metadata: dict[str, Any] | None = None,
92
+ ) -> ChainedContextEntry:
93
+ """Append a new entry, linking to the previous hash.
94
+
95
+ Args:
96
+ source: Context source (e.g. ``"user_message"``).
97
+ authority: Context authority level.
98
+ content_hash: SHA-256 of the actual content.
99
+ writer_id: Identity of the writer.
100
+ metadata: Additional context.
101
+
102
+ Returns:
103
+ The newly created chain entry.
104
+ """
105
+ prev = self.head_hash
106
+ ts = time.time()
107
+ entry_hash = _compute_entry_hash(
108
+ prev, content_hash, source, authority, writer_id, ts,
109
+ )
110
+ entry = ChainedContextEntry(
111
+ timestamp=ts,
112
+ source=source,
113
+ authority=authority,
114
+ content_hash=content_hash,
115
+ previous_hash=prev,
116
+ entry_hash=entry_hash,
117
+ writer_id=writer_id,
118
+ metadata=metadata or {},
119
+ )
120
+ self._entries.append(entry)
121
+ return entry
122
+
123
+ def verify_chain(self) -> tuple[bool, int | None]:
124
+ """Verify chain integrity.
125
+
126
+ Returns:
127
+ ``(True, None)`` if the chain is valid.
128
+ ``(False, index)`` where ``index`` is the first broken entry.
129
+ """
130
+ expected_prev = GENESIS_HASH
131
+ for i, entry in enumerate(self._entries):
132
+ if entry.previous_hash != expected_prev:
133
+ return False, i
134
+ recomputed = _compute_entry_hash(
135
+ entry.previous_hash,
136
+ entry.content_hash,
137
+ entry.source,
138
+ entry.authority,
139
+ entry.writer_id,
140
+ entry.timestamp,
141
+ )
142
+ if entry.entry_hash != recomputed:
143
+ return False, i
144
+ expected_prev = entry.entry_hash
145
+ return True, None
146
+
147
+ @property
148
+ def head_hash(self) -> str:
149
+ """Hash of the most recent entry, or GENESIS_HASH if empty."""
150
+ if not self._entries:
151
+ return GENESIS_HASH
152
+ return self._entries[-1].entry_hash
153
+
154
+ @property
155
+ def entries(self) -> list[ChainedContextEntry]:
156
+ """Read-only access to all entries."""
157
+ return list(self._entries)
158
+
159
+ def __len__(self) -> int:
160
+ return len(self._entries)
161
+
162
+ def __getitem__(self, idx: int) -> ChainedContextEntry:
163
+ return self._entries[idx]
@@ -13,6 +13,7 @@ import time
13
13
  from dataclasses import dataclass, field
14
14
  from typing import Any
15
15
 
16
+ from agentlock.chain import ContextChain
16
17
  from agentlock.schema import ContextPolicyConfig, TrustDegradationConfig
17
18
  from agentlock.types import ContextAuthority, ContextSource, DegradationEffect
18
19
 
@@ -36,6 +37,7 @@ class ContextProvenance:
36
37
  token_id: str | None = None
37
38
  session_id: str = ""
38
39
  content_hash: str = ""
40
+ previous_hash: str = ""
39
41
  parent_provenance_id: str | None = None
40
42
  metadata: dict[str, Any] = field(default_factory=dict)
41
43
 
@@ -56,6 +58,7 @@ class ContextState:
56
58
  degraded_at: float | None = None
57
59
  active_effects: list[DegradationEffect] = field(default_factory=list)
58
60
  provenance_log: list[ContextProvenance] = field(default_factory=list)
61
+ context_chain: ContextChain = field(default_factory=ContextChain)
59
62
  unattributed_count: int = 0
60
63
 
61
64
 
@@ -131,6 +134,14 @@ class ContextTracker:
131
134
  }
132
135
  authority = defaults.get(source, ContextAuthority.UNTRUSTED)
133
136
 
137
+ # Append to the hash chain and capture previous_hash
138
+ chain_entry = state.context_chain.append(
139
+ source=source.value,
140
+ authority=authority.value,
141
+ content_hash=content_hash,
142
+ writer_id=writer_id,
143
+ )
144
+
134
145
  provenance = ContextProvenance(
135
146
  source=source,
136
147
  authority=authority,
@@ -139,6 +150,7 @@ class ContextTracker:
139
150
  token_id=token_id,
140
151
  session_id=session_id,
141
152
  content_hash=content_hash,
153
+ previous_hash=chain_entry.previous_hash,
142
154
  parent_provenance_id=parent_provenance_id,
143
155
  metadata=metadata or {},
144
156
  )
@@ -151,6 +163,18 @@ class ContextTracker:
151
163
 
152
164
  return provenance
153
165
 
166
+ def verify_context_chain(self, session_id: str) -> tuple[bool, int | None]:
167
+ """Verify the hash chain integrity for a session.
168
+
169
+ Returns:
170
+ ``(True, None)`` if valid or session not found.
171
+ ``(False, index)`` if tampered at the given index.
172
+ """
173
+ state = self._states.get(session_id)
174
+ if state is None:
175
+ return True, None
176
+ return state.context_chain.verify_chain()
177
+
154
178
  def record_unattributed(self, session_id: str) -> None:
155
179
  """Record that unattributed content entered context."""
156
180
  state = self.get_or_create(session_id)