corrlog-core 0.1.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.
- corrlog_core-0.1.0/LICENSE +21 -0
- corrlog_core-0.1.0/PKG-INFO +136 -0
- corrlog_core-0.1.0/README.md +116 -0
- corrlog_core-0.1.0/corrlog_autogen/__init__.py +112 -0
- corrlog_core-0.1.0/corrlog_claude_code/__init__.py +8 -0
- corrlog_core-0.1.0/corrlog_claude_code/cli.py +147 -0
- corrlog_core-0.1.0/corrlog_claude_code/hooks.json +16 -0
- corrlog_core-0.1.0/corrlog_core/__init__.py +385 -0
- corrlog_core-0.1.0/corrlog_core/schema/acr-v1.json +97 -0
- corrlog_core-0.1.0/corrlog_core.egg-info/PKG-INFO +136 -0
- corrlog_core-0.1.0/corrlog_core.egg-info/SOURCES.txt +26 -0
- corrlog_core-0.1.0/corrlog_core.egg-info/dependency_links.txt +1 -0
- corrlog_core-0.1.0/corrlog_core.egg-info/entry_points.txt +2 -0
- corrlog_core-0.1.0/corrlog_core.egg-info/requires.txt +15 -0
- corrlog_core-0.1.0/corrlog_core.egg-info/top_level.txt +7 -0
- corrlog_core-0.1.0/corrlog_crewai/__init__.py +125 -0
- corrlog_core-0.1.0/corrlog_inspect/__init__.py +15 -0
- corrlog_core-0.1.0/corrlog_inspect/_corrlog.py +121 -0
- corrlog_core-0.1.0/corrlog_inspect/hooks.py +110 -0
- corrlog_core-0.1.0/corrlog_langchain/__init__.py +155 -0
- corrlog_core-0.1.0/corrlog_proofagent/__init__.py +202 -0
- corrlog_core-0.1.0/pyproject.toml +29 -0
- corrlog_core-0.1.0/setup.cfg +4 -0
- corrlog_core-0.1.0/tests/test_adversarial.py +97 -0
- corrlog_core-0.1.0/tests/test_core.py +111 -0
- corrlog_core-0.1.0/tests/test_fixture.py +88 -0
- corrlog_core-0.1.0/tests/test_inspect.py +249 -0
- corrlog_core-0.1.0/tests/test_proofagent.py +95 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MSSAP Consulting
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: corrlog-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Correction Record (ACR): cryptographically signed, tamper-evident records of agent self-disclosed mistakes
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: cryptography>=41.0
|
|
10
|
+
Provides-Extra: crewai
|
|
11
|
+
Requires-Dist: crewai>=0.100; extra == "crewai"
|
|
12
|
+
Provides-Extra: langchain
|
|
13
|
+
Requires-Dist: langchain>=0.3; extra == "langchain"
|
|
14
|
+
Provides-Extra: autogen
|
|
15
|
+
Requires-Dist: autogen-core>=0.4; extra == "autogen"
|
|
16
|
+
Provides-Extra: inspect
|
|
17
|
+
Requires-Dist: inspect-ai>=0.3; extra == "inspect"
|
|
18
|
+
Provides-Extra: proofagent
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# corrlog — the Agent Correction Record (ACR) SDK
|
|
22
|
+
|
|
23
|
+
**A cryptographically signed, tamper-evident record of corrections to agent actions,
|
|
24
|
+
plus the corrective action taken.**
|
|
25
|
+
|
|
26
|
+
`corrlog` extends the [AAR receipt spec](https://github.com/Cyberweasel777/agent-action-receipt-spec)
|
|
27
|
+
with the missing half of the audit ledger: the **correction** — a signed, hash-chained
|
|
28
|
+
record of "this prior action was found wrong, here's the fix and why." Receipts prove what
|
|
29
|
+
happened; corrections make the fixes you made *attributable and tamper-evident*.
|
|
30
|
+
|
|
31
|
+
- **`corrlog-core`** — framework-independent sign / verify / record / retract, Ed25519 over
|
|
32
|
+
canonical JSON (JCS), hash-chained. Single dependency: `cryptography`.
|
|
33
|
+
- **`corrlog-crewai`** — drop-in CrewAI tool-call hooks (import-safe).
|
|
34
|
+
- **`corrlog-langchain`** — `AgentMiddleware` for LangChain/LangGraph (native rollback via `Command`).
|
|
35
|
+
- **`corrlog-claude-code`** — hook CLI + `hooks.json` plugin.
|
|
36
|
+
- **`corrlog-autogen`** — `GuardedTool` wrapper over `run_json` (greenfield — no merged hook yet).
|
|
37
|
+
- **`corrlog-proofagent`** — governance gate verdict (pass/review/block) → signed `check_failed`
|
|
38
|
+
receipt, signed by the *operator's* key (import-safe, parses dataclass or dict).
|
|
39
|
+
- **`corrlog-inspect`** — `Hooks` extension for UK AISI Inspect: emits a signed receipt on an
|
|
40
|
+
`INCORRECT` score, agent identity from `spec.model` (extension package, no core changes).
|
|
41
|
+
|
|
42
|
+
## Why
|
|
43
|
+
|
|
44
|
+
Every agent vendor sells "our agent is reliable" and hides mistakes. An auditor cannot
|
|
45
|
+
distinguish "a system that never erred" (impossible) from "a system that hid its errors."
|
|
46
|
+
A signed correction log turns "we fixed it" into something *attributable and verifiable*.
|
|
47
|
+
|
|
48
|
+
## The honesty rule
|
|
49
|
+
|
|
50
|
+
An LLM does not reliably detect its own mistakes. So a correction is written when it is
|
|
51
|
+
**detected** — by a supersede, a failed check, or a human flag — never by an LLM's unaided
|
|
52
|
+
conscience. The key that holds authority over the action signs it either way (the agent's
|
|
53
|
+
key, or the runtime that holds it). The signature proves *who recorded the correction*, not
|
|
54
|
+
that the model recognised its own error.
|
|
55
|
+
|
|
56
|
+
**Scope, stated plainly:** ACR proves *integrity* (records weren't altered) and
|
|
57
|
+
*attribution* (which key signed). It does **not** prove *completeness* (that no mistakes
|
|
58
|
+
were hidden) — a party that controls its own key can simply never write a correction. See
|
|
59
|
+
SPEC.md §2 and §9 for the full boundary and the layers that narrow it.
|
|
60
|
+
|
|
61
|
+
> **Security:** the independent verifier surfaced a key-substitution vulnerability in
|
|
62
|
+
> self-authenticating verification; the spec now separates signature validity from trust
|
|
63
|
+
> and supports pinned keys. See [SECURITY.md](SECURITY.md).
|
|
64
|
+
|
|
65
|
+
| `trigger` | meaning |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `supersede` | newer write replaced an older one |
|
|
68
|
+
| `check_failed` | a guard/validator/policy rejected an action |
|
|
69
|
+
| `human_flagged` | a person marked a prior output wrong |
|
|
70
|
+
| `self_correction` | the agent detected it (weakest) |
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from corrlog_core import generate_keypair, record, retract, verify, verify_chain, MemorySink
|
|
76
|
+
|
|
77
|
+
priv, _ = generate_keypair()
|
|
78
|
+
sink = MemorySink()
|
|
79
|
+
|
|
80
|
+
# 1. Agent acts
|
|
81
|
+
r1 = record(agent_id="forecast-agent", action_type="memory.write",
|
|
82
|
+
action_args={"project": "zespri", "topic": "yield"},
|
|
83
|
+
action_result={"yield": 1420}, private_key=priv)
|
|
84
|
+
|
|
85
|
+
# 2. A check catches it's wrong -> signed correction
|
|
86
|
+
c1 = retract(prior_record=r1, reason="wrong unit (kg vs tonne)",
|
|
87
|
+
trigger="check_failed", agent_id="forecast-agent", private_key=priv,
|
|
88
|
+
fix_type="replace", corrected_content={"yield": 1.42})
|
|
89
|
+
|
|
90
|
+
# 3. Verify — offline, no trusted storage
|
|
91
|
+
assert verify(c1) # signature valid
|
|
92
|
+
assert verify_chain([r1, c1]) # hash-linked, tamper-evident
|
|
93
|
+
|
|
94
|
+
# 4. Tamper-evident: mutate the record and verification fails
|
|
95
|
+
r1["reason"] = "tampered"
|
|
96
|
+
assert not verify(r1)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Run the full demo: `python3 examples/demo.py`
|
|
100
|
+
|
|
101
|
+
## CrewAI
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from corrlog_crewai import CrewAICorrectionLog, install
|
|
105
|
+
from corrlog_core import generate_keypair, JsonlSink
|
|
106
|
+
|
|
107
|
+
priv, _ = generate_keypair()
|
|
108
|
+
log = CrewAICorrectionLog(JsonlSink("corrections.jsonl"), priv, agent_id="my-agent")
|
|
109
|
+
install(log) # registers before/after tool-call hooks globally
|
|
110
|
+
|
|
111
|
+
# A human can flag a prior record wrong at any time:
|
|
112
|
+
log.mark_wrong(prior_record, "wrong amount", fix_note="corrected")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Compliance mapping (honest)
|
|
116
|
+
|
|
117
|
+
ACR *supports* — it does not certify — EU AI Act duties for in-scope systems:
|
|
118
|
+
Art 26(5) monitor/suspend, Art 26(6) log retention ≥6 months, Art 20 corrective actions,
|
|
119
|
+
Art 73 serious-incident reporting; NIST AI RMF MANAGE 4.3. See SPEC.md §8 for what we can
|
|
120
|
+
and cannot claim.
|
|
121
|
+
|
|
122
|
+
## Status
|
|
123
|
+
|
|
124
|
+
`corrlog-core` implemented and tested (core + adversarial + verifier-fixture suites).
|
|
125
|
+
Adapters for CrewAI, LangChain/LangGraph, Claude Code, and AutoGen are implemented and
|
|
126
|
+
import-safe. Two keyed-detector adapters ship too: `corrlog-proofagent` (governance gate
|
|
127
|
+
→ signed receipt) and `corrlog-inspect` (Inspect `Hooks` extension), both tested against
|
|
128
|
+
the real upstream APIs with real signing verified end-to-end. See SPEC.md for the
|
|
129
|
+
integration points and §9 for the completeness roadmap (gapless sequence, external
|
|
130
|
+
anchor, keyed trigger authorities).
|
|
131
|
+
|
|
132
|
+
## Tests
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
python3 tests/test_core.py
|
|
136
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# corrlog — the Agent Correction Record (ACR) SDK
|
|
2
|
+
|
|
3
|
+
**A cryptographically signed, tamper-evident record of corrections to agent actions,
|
|
4
|
+
plus the corrective action taken.**
|
|
5
|
+
|
|
6
|
+
`corrlog` extends the [AAR receipt spec](https://github.com/Cyberweasel777/agent-action-receipt-spec)
|
|
7
|
+
with the missing half of the audit ledger: the **correction** — a signed, hash-chained
|
|
8
|
+
record of "this prior action was found wrong, here's the fix and why." Receipts prove what
|
|
9
|
+
happened; corrections make the fixes you made *attributable and tamper-evident*.
|
|
10
|
+
|
|
11
|
+
- **`corrlog-core`** — framework-independent sign / verify / record / retract, Ed25519 over
|
|
12
|
+
canonical JSON (JCS), hash-chained. Single dependency: `cryptography`.
|
|
13
|
+
- **`corrlog-crewai`** — drop-in CrewAI tool-call hooks (import-safe).
|
|
14
|
+
- **`corrlog-langchain`** — `AgentMiddleware` for LangChain/LangGraph (native rollback via `Command`).
|
|
15
|
+
- **`corrlog-claude-code`** — hook CLI + `hooks.json` plugin.
|
|
16
|
+
- **`corrlog-autogen`** — `GuardedTool` wrapper over `run_json` (greenfield — no merged hook yet).
|
|
17
|
+
- **`corrlog-proofagent`** — governance gate verdict (pass/review/block) → signed `check_failed`
|
|
18
|
+
receipt, signed by the *operator's* key (import-safe, parses dataclass or dict).
|
|
19
|
+
- **`corrlog-inspect`** — `Hooks` extension for UK AISI Inspect: emits a signed receipt on an
|
|
20
|
+
`INCORRECT` score, agent identity from `spec.model` (extension package, no core changes).
|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
Every agent vendor sells "our agent is reliable" and hides mistakes. An auditor cannot
|
|
25
|
+
distinguish "a system that never erred" (impossible) from "a system that hid its errors."
|
|
26
|
+
A signed correction log turns "we fixed it" into something *attributable and verifiable*.
|
|
27
|
+
|
|
28
|
+
## The honesty rule
|
|
29
|
+
|
|
30
|
+
An LLM does not reliably detect its own mistakes. So a correction is written when it is
|
|
31
|
+
**detected** — by a supersede, a failed check, or a human flag — never by an LLM's unaided
|
|
32
|
+
conscience. The key that holds authority over the action signs it either way (the agent's
|
|
33
|
+
key, or the runtime that holds it). The signature proves *who recorded the correction*, not
|
|
34
|
+
that the model recognised its own error.
|
|
35
|
+
|
|
36
|
+
**Scope, stated plainly:** ACR proves *integrity* (records weren't altered) and
|
|
37
|
+
*attribution* (which key signed). It does **not** prove *completeness* (that no mistakes
|
|
38
|
+
were hidden) — a party that controls its own key can simply never write a correction. See
|
|
39
|
+
SPEC.md §2 and §9 for the full boundary and the layers that narrow it.
|
|
40
|
+
|
|
41
|
+
> **Security:** the independent verifier surfaced a key-substitution vulnerability in
|
|
42
|
+
> self-authenticating verification; the spec now separates signature validity from trust
|
|
43
|
+
> and supports pinned keys. See [SECURITY.md](SECURITY.md).
|
|
44
|
+
|
|
45
|
+
| `trigger` | meaning |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `supersede` | newer write replaced an older one |
|
|
48
|
+
| `check_failed` | a guard/validator/policy rejected an action |
|
|
49
|
+
| `human_flagged` | a person marked a prior output wrong |
|
|
50
|
+
| `self_correction` | the agent detected it (weakest) |
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from corrlog_core import generate_keypair, record, retract, verify, verify_chain, MemorySink
|
|
56
|
+
|
|
57
|
+
priv, _ = generate_keypair()
|
|
58
|
+
sink = MemorySink()
|
|
59
|
+
|
|
60
|
+
# 1. Agent acts
|
|
61
|
+
r1 = record(agent_id="forecast-agent", action_type="memory.write",
|
|
62
|
+
action_args={"project": "zespri", "topic": "yield"},
|
|
63
|
+
action_result={"yield": 1420}, private_key=priv)
|
|
64
|
+
|
|
65
|
+
# 2. A check catches it's wrong -> signed correction
|
|
66
|
+
c1 = retract(prior_record=r1, reason="wrong unit (kg vs tonne)",
|
|
67
|
+
trigger="check_failed", agent_id="forecast-agent", private_key=priv,
|
|
68
|
+
fix_type="replace", corrected_content={"yield": 1.42})
|
|
69
|
+
|
|
70
|
+
# 3. Verify — offline, no trusted storage
|
|
71
|
+
assert verify(c1) # signature valid
|
|
72
|
+
assert verify_chain([r1, c1]) # hash-linked, tamper-evident
|
|
73
|
+
|
|
74
|
+
# 4. Tamper-evident: mutate the record and verification fails
|
|
75
|
+
r1["reason"] = "tampered"
|
|
76
|
+
assert not verify(r1)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Run the full demo: `python3 examples/demo.py`
|
|
80
|
+
|
|
81
|
+
## CrewAI
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from corrlog_crewai import CrewAICorrectionLog, install
|
|
85
|
+
from corrlog_core import generate_keypair, JsonlSink
|
|
86
|
+
|
|
87
|
+
priv, _ = generate_keypair()
|
|
88
|
+
log = CrewAICorrectionLog(JsonlSink("corrections.jsonl"), priv, agent_id="my-agent")
|
|
89
|
+
install(log) # registers before/after tool-call hooks globally
|
|
90
|
+
|
|
91
|
+
# A human can flag a prior record wrong at any time:
|
|
92
|
+
log.mark_wrong(prior_record, "wrong amount", fix_note="corrected")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Compliance mapping (honest)
|
|
96
|
+
|
|
97
|
+
ACR *supports* — it does not certify — EU AI Act duties for in-scope systems:
|
|
98
|
+
Art 26(5) monitor/suspend, Art 26(6) log retention ≥6 months, Art 20 corrective actions,
|
|
99
|
+
Art 73 serious-incident reporting; NIST AI RMF MANAGE 4.3. See SPEC.md §8 for what we can
|
|
100
|
+
and cannot claim.
|
|
101
|
+
|
|
102
|
+
## Status
|
|
103
|
+
|
|
104
|
+
`corrlog-core` implemented and tested (core + adversarial + verifier-fixture suites).
|
|
105
|
+
Adapters for CrewAI, LangChain/LangGraph, Claude Code, and AutoGen are implemented and
|
|
106
|
+
import-safe. Two keyed-detector adapters ship too: `corrlog-proofagent` (governance gate
|
|
107
|
+
→ signed receipt) and `corrlog-inspect` (Inspect `Hooks` extension), both tested against
|
|
108
|
+
the real upstream APIs with real signing verified end-to-end. See SPEC.md for the
|
|
109
|
+
integration points and §9 for the completeness roadmap (gapless sequence, external
|
|
110
|
+
anchor, keyed trigger authorities).
|
|
111
|
+
|
|
112
|
+
## Tests
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
python3 tests/test_core.py
|
|
116
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""corrlog-autogen — drop-in correction log for AutoGen agents.
|
|
2
|
+
|
|
3
|
+
AutoGen has NO merged tool-call middleware as of this build (verified against the
|
|
4
|
+
cloned source). The seams we need — `BaseTool.run_json()` / `Workbench.call_tool()` —
|
|
5
|
+
are exactly what the still-open proposals target (#7405 GuardrailProvider, #7353 AAR
|
|
6
|
+
receipts). So this adapter wraps `run_json` rather than relying on a shipped hook.
|
|
7
|
+
|
|
8
|
+
Import-safe: if autogen is not installed, `is_available()` returns False and the
|
|
9
|
+
wrapper class still imports.
|
|
10
|
+
|
|
11
|
+
Usage (when autogen is installed):
|
|
12
|
+
from corrlog_autogen import GuardedTool
|
|
13
|
+
tool = GuardedTool(my_autogen_tool, sink, private_key, agent_id="agent-1")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from corrlog_core import record, retract
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from autogen_core.tools import BaseTool # type: ignore
|
|
24
|
+
_AUTOGEN_AVAILABLE = True
|
|
25
|
+
except Exception: # pragma: no cover
|
|
26
|
+
_AUTOGEN_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_available() -> bool:
|
|
30
|
+
return _AUTOGEN_AVAILABLE
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GuardedTool:
|
|
34
|
+
"""Wraps an AutoGen tool, adding before/after correction-logging around `run_json`.
|
|
35
|
+
|
|
36
|
+
Structured to match the proposed GuardrailProvider signature from #7405, so it
|
|
37
|
+
upgrades cleanly when that lands upstream.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, tool, sink, private_key, agent_id: str, principal_id: str = "organization") -> None:
|
|
41
|
+
self._tool = tool
|
|
42
|
+
self.sink = sink
|
|
43
|
+
self.private_key = private_key
|
|
44
|
+
self.agent_id = agent_id
|
|
45
|
+
self.principal_id = principal_id
|
|
46
|
+
|
|
47
|
+
def __getattr__(self, name: str):
|
|
48
|
+
# Delegate everything else to the wrapped tool.
|
|
49
|
+
return getattr(self._tool, name)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def name(self) -> str:
|
|
53
|
+
return getattr(self._tool, "name", "unknown_tool")
|
|
54
|
+
|
|
55
|
+
async def run_json(self, args, cancellation_token=None, call_id=None):
|
|
56
|
+
tool_name = self.name
|
|
57
|
+
rec = record(
|
|
58
|
+
agent_id=self.agent_id,
|
|
59
|
+
principal_id=self.principal_id,
|
|
60
|
+
action_type=f"autogen.tool.{tool_name}",
|
|
61
|
+
action_args=args if isinstance(args, dict) else {"args": args},
|
|
62
|
+
private_key=self.private_key,
|
|
63
|
+
metadata={"call_id": call_id} if call_id else None,
|
|
64
|
+
)
|
|
65
|
+
self.sink.append(rec)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
result = await self._tool.run_json(args, cancellation_token, call_id)
|
|
69
|
+
except Exception as e: # noqa: BLE001
|
|
70
|
+
corr = retract(
|
|
71
|
+
prior_record=rec,
|
|
72
|
+
reason=f"tool raised: {e}",
|
|
73
|
+
trigger="check_failed",
|
|
74
|
+
agent_id=self.agent_id,
|
|
75
|
+
private_key=self.private_key,
|
|
76
|
+
principal_id=self.principal_id,
|
|
77
|
+
fix_type="replace",
|
|
78
|
+
fix_note=f"autogen tool {tool_name} raised",
|
|
79
|
+
)
|
|
80
|
+
self.sink.append(corr)
|
|
81
|
+
raise
|
|
82
|
+
|
|
83
|
+
# Result-level error detection (best-effort, framework-dependent shape).
|
|
84
|
+
if isinstance(result, dict) and result.get("error"):
|
|
85
|
+
corr = retract(
|
|
86
|
+
prior_record=rec,
|
|
87
|
+
reason=str(result["error"]),
|
|
88
|
+
trigger="check_failed",
|
|
89
|
+
agent_id=self.agent_id,
|
|
90
|
+
private_key=self.private_key,
|
|
91
|
+
principal_id=self.principal_id,
|
|
92
|
+
fix_type="replace",
|
|
93
|
+
fix_note=f"autogen tool {tool_name} errored",
|
|
94
|
+
)
|
|
95
|
+
self.sink.append(corr)
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
def mark_wrong(self, prior_record: dict[str, Any], reason: str, fix_note: str | None = None) -> dict[str, Any]:
|
|
100
|
+
"""Human-flag a prior record as wrong -> signed correction."""
|
|
101
|
+
corr = retract(
|
|
102
|
+
prior_record=prior_record,
|
|
103
|
+
reason=reason,
|
|
104
|
+
trigger="human_flagged",
|
|
105
|
+
agent_id=self.agent_id,
|
|
106
|
+
private_key=self.private_key,
|
|
107
|
+
principal_id=self.principal_id,
|
|
108
|
+
fix_type="replace",
|
|
109
|
+
fix_note=fix_note,
|
|
110
|
+
)
|
|
111
|
+
self.sink.append(corr)
|
|
112
|
+
return corr
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""corrlog-claude-code — CLI for Claude Code hooks.
|
|
2
|
+
|
|
3
|
+
Invoked by hooks/hooks.json as an external process. Reads the hook's stdin JSON,
|
|
4
|
+
emits a signed correction-log record.
|
|
5
|
+
|
|
6
|
+
Modes:
|
|
7
|
+
- PreToolUse : hash the tool input, mint an action id (emit to stderr-free stdout)
|
|
8
|
+
- PostToolUse : hash the tool result, sign the action record
|
|
9
|
+
- human-flag : `corrlog human-flag --receipt <id> --reason "..."` to flag wrong
|
|
10
|
+
|
|
11
|
+
The key lives in $CORRLOG_KEY_PATH (a PEM/raw Ed25519 private key file).
|
|
12
|
+
Records append to $CORRLOG_SINK (JSONL path) or ./corrections.jsonl.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
23
|
+
|
|
24
|
+
from corrlog_core import generate_keypair, record, retract, JsonlSink
|
|
25
|
+
from cryptography.hazmat.primitives import serialization
|
|
26
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_or_create_key(path: str):
|
|
30
|
+
if path and os.path.exists(path):
|
|
31
|
+
with open(path, "rb") as f:
|
|
32
|
+
return serialization.load_pem_private_key(f.read(), password=None)
|
|
33
|
+
priv = ed25519.Ed25519PrivateKey.generate()
|
|
34
|
+
if path:
|
|
35
|
+
with open(path, "wb") as f:
|
|
36
|
+
f.write(priv.private_bytes(
|
|
37
|
+
serialization.Encoding.PEM,
|
|
38
|
+
serialization.PrivateFormat.PKCS8,
|
|
39
|
+
serialization.NoEncryption(),
|
|
40
|
+
))
|
|
41
|
+
return priv
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _sink_path() -> str:
|
|
45
|
+
return os.environ.get("CORRLOG_SINK", os.path.join(os.getcwd(), "corrections.jsonl"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _key_path() -> str:
|
|
49
|
+
return os.environ.get("CORRLOG_KEY_PATH", os.path.join(os.getcwd(), "corrlog_key.pem"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _agent_id() -> str:
|
|
53
|
+
return os.environ.get("CORRLOG_AGENT_ID", "claude-code-agent")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main(argv: list[str] | None = None) -> int:
|
|
57
|
+
argv = argv if argv is not None else sys.argv[1:]
|
|
58
|
+
|
|
59
|
+
# Subcommand: human-flag
|
|
60
|
+
if argv and argv[0] == "human-flag":
|
|
61
|
+
p = argparse.ArgumentParser(prog="corrlog human-flag")
|
|
62
|
+
p.add_argument("--receipt", required=True)
|
|
63
|
+
p.add_argument("--reason", required=True)
|
|
64
|
+
p.add_argument("--fix-note")
|
|
65
|
+
args = p.parse_args(argv[1:])
|
|
66
|
+
priv = _load_or_create_key(_key_path())
|
|
67
|
+
sink = JsonlSink(_sink_path())
|
|
68
|
+
# Find the prior record.
|
|
69
|
+
prior = sink.get(args.receipt)
|
|
70
|
+
if prior is None:
|
|
71
|
+
print(json.dumps({"ok": False, "error": f"receipt {args.receipt} not found"}), file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
corr = retract(
|
|
74
|
+
prior_record=prior, reason=args.reason, trigger="human_flagged",
|
|
75
|
+
agent_id=_agent_id(), private_key=priv, fix_type="replace", fix_note=args.fix_note,
|
|
76
|
+
)
|
|
77
|
+
sink.append(corr)
|
|
78
|
+
print(json.dumps({"ok": True, "correctionId": corr["correctionId"]}))
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
# Hook mode: read stdin JSON (Claude Code hook contract).
|
|
82
|
+
try:
|
|
83
|
+
data = json.load(sys.stdin)
|
|
84
|
+
except Exception:
|
|
85
|
+
data = {}
|
|
86
|
+
|
|
87
|
+
hook = data.get("hook_event_name", "")
|
|
88
|
+
tool_name = data.get("tool_name", "")
|
|
89
|
+
tool_input = data.get("tool_input", {})
|
|
90
|
+
|
|
91
|
+
priv = _load_or_create_key(_key_path())
|
|
92
|
+
sink = JsonlSink(_sink_path())
|
|
93
|
+
|
|
94
|
+
if hook == "PreToolUse":
|
|
95
|
+
rec = record(
|
|
96
|
+
agent_id=_agent_id(), action_type=f"claude.tool.{tool_name}",
|
|
97
|
+
action_args=tool_input if isinstance(tool_input, dict) else {"input": tool_input},
|
|
98
|
+
private_key=priv,
|
|
99
|
+
)
|
|
100
|
+
sink.append(rec)
|
|
101
|
+
# Allow the call; stash the action id in a sidecar file for PostToolUse.
|
|
102
|
+
_sidecar = os.path.join(os.getcwd(), ".corrlog_inflight.json")
|
|
103
|
+
try:
|
|
104
|
+
inflight = json.load(open(_sidecar)) if os.path.exists(_sidecar) else {}
|
|
105
|
+
inflight[tool_name] = rec["correctionId"]
|
|
106
|
+
json.dump(inflight, open(_sidecar, "w"))
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
print(json.dumps({"continue": True}))
|
|
110
|
+
|
|
111
|
+
elif hook == "PostToolUse":
|
|
112
|
+
tool_result = data.get("tool_result", "")
|
|
113
|
+
_sidecar = os.path.join(os.getcwd(), ".corrlog_inflight.json")
|
|
114
|
+
prior_id = None
|
|
115
|
+
try:
|
|
116
|
+
inflight = json.load(open(_sidecar)) if os.path.exists(_sidecar) else {}
|
|
117
|
+
prior_id = inflight.pop(tool_name, None)
|
|
118
|
+
json.dump(inflight, open(_sidecar, "w"))
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
rec = record(
|
|
122
|
+
agent_id=_agent_id(), action_type=f"claude.tool.{tool_name}",
|
|
123
|
+
action_args=tool_input if isinstance(tool_input, dict) else {"input": tool_input},
|
|
124
|
+
action_result=tool_result, private_key=priv,
|
|
125
|
+
)
|
|
126
|
+
sink.append(rec)
|
|
127
|
+
# Detect error in result -> check_failed correction.
|
|
128
|
+
if isinstance(tool_result, str) and any(
|
|
129
|
+
m in tool_result.lower() for m in ("error", "failed", "exception", "denied")
|
|
130
|
+
):
|
|
131
|
+
corr = retract(
|
|
132
|
+
prior_record=rec, reason="tool returned error", trigger="check_failed",
|
|
133
|
+
agent_id=_agent_id(), private_key=priv, fix_type="replace",
|
|
134
|
+
fix_note=f"claude tool {tool_name} errored",
|
|
135
|
+
)
|
|
136
|
+
sink.append(corr)
|
|
137
|
+
print(json.dumps({"continue": True}))
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
# Unhandled hook event: no-op, allow.
|
|
141
|
+
print(json.dumps({"continue": True}))
|
|
142
|
+
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
sys.exit(main())
|