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.
@@ -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,8 @@
1
+ """corrlog-claude-code — plugin surface. The actual hook logic is in cli.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def is_available() -> bool:
7
+ """Claude Code hooks are external processes — always 'available' if python is."""
8
+ return True
@@ -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())
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "*",
6
+ "command": "python3 -m corrlog_claude_code.cli"
7
+ }
8
+ ],
9
+ "PostToolUse": [
10
+ {
11
+ "matcher": "*",
12
+ "command": "python3 -m corrlog_claude_code.cli"
13
+ }
14
+ ]
15
+ }
16
+ }