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.
- {agentlock-1.2.0 → agentlock-1.2.1}/CHANGELOG.md +39 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/PKG-INFO +37 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/README.md +33 -1
- agentlock-1.2.1/SECURITY.md +139 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/__init__.py +34 -22
- agentlock-1.2.1/agentlock/chain.py +163 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/context.py +24 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/defer.py +73 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/gate.py +290 -11
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/hardening.py +2 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/modify.py +8 -2
- agentlock-1.2.1/agentlock/receipts.py +211 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/schema.py +1 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/combos.py +3 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/echo.py +0 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/prompt_scan.py +1 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/velocity.py +1 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/stepup.py +1 -1
- {agentlock-1.2.0 → agentlock-1.2.1}/docs/specification.md +94 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/examples/fastapi_app.py +3 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/examples/rate_limiting.py +1 -1
- {agentlock-1.2.0 → agentlock-1.2.1}/pyproject.toml +6 -1
- agentlock-1.2.1/schema/agentlock-v1.2.json +744 -0
- agentlock-1.2.1/tests/test_benchmark_gaps.py +543 -0
- agentlock-1.2.1/tests/test_benchmark_gaps_v2.py +212 -0
- agentlock-1.2.1/tests/test_chain.py +420 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_cli.py +1 -1
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_combos.py +0 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_compound.py +0 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_defer.py +2 -4
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_echo.py +0 -3
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_gate_v12.py +0 -3
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_hardening.py +21 -17
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_modify.py +0 -2
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_prompt_scan.py +27 -14
- agentlock-1.2.1/tests/test_receipts.py +392 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_stepup.py +5 -7
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_velocity.py +0 -2
- agentlock-1.2.0/SECURITY.md +0 -89
- {agentlock-1.2.0 → agentlock-1.2.1}/.github/workflows/ci.yml +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/.gitignore +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/CONTRIBUTING.md +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/LICENSE +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/NOTICE +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/audit.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/auth_providers/__init__.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/cli.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/decorators.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/exceptions.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/__init__.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/autogen.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/crewai.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/fastapi.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/flask.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/langchain.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/integrations/mcp.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/memory_gate.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/policy.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/py.typed +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/rate_limit.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/redaction.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/session.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/signals/__init__.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/token.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/agentlock/types.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/docs/benchmark.md +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/docs/integrations.md +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/docs/quickstart.md +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/examples/data_redaction.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/examples/decorator_example.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/examples/multi_role.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/examples/quickstart.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/schema/agentlock-v1.0.json +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/__init__.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/conftest.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_audit.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_audit_v11.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_backward_compat.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_context.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_decorators.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_filter_pipeline.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_gate.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_gate_v11.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_memory_gate.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_pii_defense.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_policy.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_policy_v11.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_rate_limit.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_redaction.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_schema.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_schema_v11.py +0 -0
- {agentlock-1.2.0 → agentlock-1.2.1}/tests/test_session.py +0 -0
- {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.
|
|
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,
|
|
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,
|
|
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.
|
|
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)
|