strata-agent-memory 0.2.0__tar.gz → 0.2.2__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.
- {strata_agent_memory-0.2.0/strata_agent_memory.egg-info → strata_agent_memory-0.2.2}/PKG-INFO +26 -5
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/README.md +25 -4
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/__init__.py +1 -1
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/audit.py +148 -14
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/cli.py +509 -60
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/compact.py +5 -2
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/compiler.py +88 -3
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/contracts.py +193 -31
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/evidence.py +365 -57
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/export.py +14 -5
- strata_agent_memory-0.2.2/agent_memory/hooks.py +1085 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/identity.py +26 -2
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/ledger.py +201 -104
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/mcp_server.py +65 -20
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/recheck.py +14 -3
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/records.py +198 -6
- strata_agent_memory-0.2.2/agent_memory/relay/__init__.py +9 -0
- strata_agent_memory-0.2.2/agent_memory/relay/adapters.py +521 -0
- strata_agent_memory-0.2.2/agent_memory/relay/cli.py +226 -0
- strata_agent_memory-0.2.2/agent_memory/relay/client.py +843 -0
- strata_agent_memory-0.2.2/agent_memory/relay/core.py +821 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/reliability.py +21 -4
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/remote.py +99 -3
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/render.py +39 -3
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/serve.py +279 -34
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/signing.py +122 -18
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/trust.py +118 -36
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/pyproject.toml +2 -2
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2/strata_agent_memory.egg-info}/PKG-INFO +26 -5
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/SOURCES.txt +11 -0
- strata_agent_memory-0.2.2/tests/test_audit.py +485 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_cli.py +65 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_compiler_render.py +192 -1
- strata_agent_memory-0.2.2/tests/test_contracts.py +511 -0
- strata_agent_memory-0.2.2/tests/test_deploy_relay.py +652 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_deploy_replica.py +210 -2
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_deploy_tokens.py +68 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_evidence.py +72 -19
- strata_agent_memory-0.2.2/tests/test_hooks.py +1571 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_identity.py +47 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_ledger.py +58 -12
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_packaging.py +4 -1
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_pathmap.py +10 -2
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_phase2.py +103 -5
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_recheck.py +49 -2
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_records.py +108 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_redaction.py +27 -0
- strata_agent_memory-0.2.2/tests/test_relay_client.py +1420 -0
- strata_agent_memory-0.2.2/tests/test_relay_codex.py +381 -0
- strata_agent_memory-0.2.2/tests/test_relay_core.py +966 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_reliability.py +3 -1
- strata_agent_memory-0.2.2/tests/test_remote.py +1217 -0
- strata_agent_memory-0.2.2/tests/test_remote_signing.py +205 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_review2_fixes.py +14 -1
- strata_agent_memory-0.2.2/tests/test_review3_fixes.py +680 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_scale.py +50 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_serve.py +660 -23
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_trust.py +41 -3
- strata_agent_memory-0.2.0/agent_memory/hooks.py +0 -458
- strata_agent_memory-0.2.0/tests/test_audit.py +0 -229
- strata_agent_memory-0.2.0/tests/test_contracts.py +0 -292
- strata_agent_memory-0.2.0/tests/test_hooks.py +0 -556
- strata_agent_memory-0.2.0/tests/test_remote.py +0 -525
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/LICENSE +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/__main__.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/memorizer.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/pathmap.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/stats.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/setup.cfg +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/entry_points.txt +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/requires.txt +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/top_level.txt +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_compact.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_issue_bridge.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_ledger_index.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_memorizer.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_review_fixes.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_signing.py +0 -0
- {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_stats.py +0 -0
{strata_agent_memory-0.2.0/strata_agent_memory.egg-info → strata_agent_memory-0.2.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: strata-agent-memory
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Portable, auditable, cross-vendor memory for LLM agents: evidence-bound claims, trust tiers, and compiled context views.
|
|
5
5
|
Author: Strata Intelligence
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -45,9 +45,13 @@ service, no dependencies, Python 3.10+.
|
|
|
45
45
|
commit/file hash. The claim is rejected otherwise. A receipt is the
|
|
46
46
|
filer's record; the proof is the re-run, which `verify` and the nightly
|
|
47
47
|
lane perform, and the audit lists what nobody has re-run.
|
|
48
|
-
- **
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
- **Self-review is not verification.** A claim reaches `verified` when a
|
|
49
|
+
person, or an agent that is not its author, re-runs the proof; the author's
|
|
50
|
+
own re-run stays `self-verified`. A fleet on one vendor gets the same rigor
|
|
51
|
+
as a mixed one. A ledger that wants the stronger rule sets
|
|
52
|
+
`cross_vendor_required`, and then only a person or another vendor's agent
|
|
53
|
+
(Codex, Gemini, a local Qwen) promotes; either way the audit says which
|
|
54
|
+
lineage verified what.
|
|
51
55
|
- **Refuting a claim taints everything built on it.** Retractions walk the
|
|
52
56
|
provenance graph; nothing is deleted; the audit prints the chain.
|
|
53
57
|
- **Verifiers are isolated, workers fork.** A reviewer never sees the
|
|
@@ -103,6 +107,20 @@ Inside Claude Code, Codex or Gemini the same operations are MCP tools
|
|
|
103
107
|
(`memory_run`, `memory_claim`, `memory_verify`, `memory_compile`, …); see
|
|
104
108
|
`examples/` and [docs/INTEGRATION.md](docs/INTEGRATION.md).
|
|
105
109
|
|
|
110
|
+
Agents also talk to each other on this ledger, with no broker and no mail
|
|
111
|
+
server: a message is a `note` claim tagged `to:<agent>`, and `agent-memory
|
|
112
|
+
relay` is one small process per agent, with no model in it, that tails the
|
|
113
|
+
replica's `/events` for the records addressed to its agent, debounces them
|
|
114
|
+
into a batch, applies circuit breakers before anything is woken (per-thread
|
|
115
|
+
rate, consecutive machine-only turns, per-agent wake ceilings, a token
|
|
116
|
+
budget), renders the batch inside a fixed delimiter as data rather than
|
|
117
|
+
instructions, hands it to the harness through one adapter, and files an ack
|
|
118
|
+
record for each message it delivered. It listens on no port and spends
|
|
119
|
+
nothing while it waits. The kit is `deploy/relay/`, the page to run it from is
|
|
120
|
+
[docs/RUNBOOK-RELAY.md](docs/RUNBOOK-RELAY.md), the conventions are
|
|
121
|
+
[docs/PROTOCOL.md](docs/PROTOCOL.md) section 11, and why it is shaped this way
|
|
122
|
+
is [docs/COMMS-BUS-PLAN.md](docs/COMMS-BUS-PLAN.md).
|
|
123
|
+
|
|
106
124
|
## Where this goes: measured reliability, then judged claims
|
|
107
125
|
|
|
108
126
|
Memory is often perception, not fact. The alpha separates the two
|
|
@@ -167,8 +185,10 @@ agent_memory/ the package (stdlib only)
|
|
|
167
185
|
pathmap.py answering another machine's receipts
|
|
168
186
|
signing.py optional Ed25519 attestations (DSSE); needs strata-agent-memory[sign]
|
|
169
187
|
hooks.py hook adapters: Claude Code, Codex CLI, Gemini CLI, Cursor
|
|
188
|
+
relay/ the message relay: core (batching, breakers, the delimiter), client (the /events tail), adapters, cli
|
|
170
189
|
cli.py agent-memory <command>
|
|
171
|
-
docs/ DESIGN.md, PROTOCOL.md, RESEARCH-SYNTHESIS.md, INTEGRATION.md
|
|
190
|
+
docs/ DESIGN.md, PROTOCOL.md, RESEARCH-SYNTHESIS.md, INTEGRATION.md, COMMS-BUS-PLAN.md, RUNBOOK-*.md
|
|
191
|
+
deploy/ replica/ (a hosted ledger) and relay/ (a relay per agent), systemd and launchd
|
|
172
192
|
schemas/ JSON Schema for the record envelope and bodies
|
|
173
193
|
examples/ Claude Code, Codex, Gemini, local LLM, CI
|
|
174
194
|
tests/ pytest suite; tests/sit/phase3/ is the CLI-driven system test
|
|
@@ -180,6 +200,7 @@ tests/ pytest suite; tests/sit/phase3/ is the CLI-driven system test
|
|
|
180
200
|
- [docs/PROTOCOL.md](docs/PROTOCOL.md): the normative format, filing rules, trust computation and compile semantics, for implementers in any language.
|
|
181
201
|
- [docs/RESEARCH-SYNTHESIS.md](docs/RESEARCH-SYNTHESIS.md): the nine sources, what was taken from each, and what none of them had.
|
|
182
202
|
- [docs/INTEGRATION.md](docs/INTEGRATION.md): setup per harness, the local verifier lane, the contract loop.
|
|
203
|
+
- [docs/COMMS-BUS-PLAN.md](docs/COMMS-BUS-PLAN.md): how agents talk through the ledger without spending tokens while they wait, and [docs/RUNBOOK-RELAY.md](docs/RUNBOOK-RELAY.md) to run a relay per agent.
|
|
183
204
|
- [docs/LANDSCAPE.md](docs/LANDSCAPE.md): the competing systems, their verified licenses, the standards receipts align with, and what none of them do.
|
|
184
205
|
- [docs/UAT-PLAN.md](docs/UAT-PLAN.md): the one-week acceptance test across Claude Code, Codex, Antigravity and Grok.
|
|
185
206
|
- [docs/UAT-SELF.md](docs/UAT-SELF.md): the same test for one person with two subscriptions, Claude Code and Codex CLI, with `scripts/uat/` to set it up and to run the cross-lineage verification.
|
|
@@ -21,9 +21,13 @@ service, no dependencies, Python 3.10+.
|
|
|
21
21
|
commit/file hash. The claim is rejected otherwise. A receipt is the
|
|
22
22
|
filer's record; the proof is the re-run, which `verify` and the nightly
|
|
23
23
|
lane perform, and the audit lists what nobody has re-run.
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
- **Self-review is not verification.** A claim reaches `verified` when a
|
|
25
|
+
person, or an agent that is not its author, re-runs the proof; the author's
|
|
26
|
+
own re-run stays `self-verified`. A fleet on one vendor gets the same rigor
|
|
27
|
+
as a mixed one. A ledger that wants the stronger rule sets
|
|
28
|
+
`cross_vendor_required`, and then only a person or another vendor's agent
|
|
29
|
+
(Codex, Gemini, a local Qwen) promotes; either way the audit says which
|
|
30
|
+
lineage verified what.
|
|
27
31
|
- **Refuting a claim taints everything built on it.** Retractions walk the
|
|
28
32
|
provenance graph; nothing is deleted; the audit prints the chain.
|
|
29
33
|
- **Verifiers are isolated, workers fork.** A reviewer never sees the
|
|
@@ -79,6 +83,20 @@ Inside Claude Code, Codex or Gemini the same operations are MCP tools
|
|
|
79
83
|
(`memory_run`, `memory_claim`, `memory_verify`, `memory_compile`, …); see
|
|
80
84
|
`examples/` and [docs/INTEGRATION.md](docs/INTEGRATION.md).
|
|
81
85
|
|
|
86
|
+
Agents also talk to each other on this ledger, with no broker and no mail
|
|
87
|
+
server: a message is a `note` claim tagged `to:<agent>`, and `agent-memory
|
|
88
|
+
relay` is one small process per agent, with no model in it, that tails the
|
|
89
|
+
replica's `/events` for the records addressed to its agent, debounces them
|
|
90
|
+
into a batch, applies circuit breakers before anything is woken (per-thread
|
|
91
|
+
rate, consecutive machine-only turns, per-agent wake ceilings, a token
|
|
92
|
+
budget), renders the batch inside a fixed delimiter as data rather than
|
|
93
|
+
instructions, hands it to the harness through one adapter, and files an ack
|
|
94
|
+
record for each message it delivered. It listens on no port and spends
|
|
95
|
+
nothing while it waits. The kit is `deploy/relay/`, the page to run it from is
|
|
96
|
+
[docs/RUNBOOK-RELAY.md](docs/RUNBOOK-RELAY.md), the conventions are
|
|
97
|
+
[docs/PROTOCOL.md](docs/PROTOCOL.md) section 11, and why it is shaped this way
|
|
98
|
+
is [docs/COMMS-BUS-PLAN.md](docs/COMMS-BUS-PLAN.md).
|
|
99
|
+
|
|
82
100
|
## Where this goes: measured reliability, then judged claims
|
|
83
101
|
|
|
84
102
|
Memory is often perception, not fact. The alpha separates the two
|
|
@@ -143,8 +161,10 @@ agent_memory/ the package (stdlib only)
|
|
|
143
161
|
pathmap.py answering another machine's receipts
|
|
144
162
|
signing.py optional Ed25519 attestations (DSSE); needs strata-agent-memory[sign]
|
|
145
163
|
hooks.py hook adapters: Claude Code, Codex CLI, Gemini CLI, Cursor
|
|
164
|
+
relay/ the message relay: core (batching, breakers, the delimiter), client (the /events tail), adapters, cli
|
|
146
165
|
cli.py agent-memory <command>
|
|
147
|
-
docs/ DESIGN.md, PROTOCOL.md, RESEARCH-SYNTHESIS.md, INTEGRATION.md
|
|
166
|
+
docs/ DESIGN.md, PROTOCOL.md, RESEARCH-SYNTHESIS.md, INTEGRATION.md, COMMS-BUS-PLAN.md, RUNBOOK-*.md
|
|
167
|
+
deploy/ replica/ (a hosted ledger) and relay/ (a relay per agent), systemd and launchd
|
|
148
168
|
schemas/ JSON Schema for the record envelope and bodies
|
|
149
169
|
examples/ Claude Code, Codex, Gemini, local LLM, CI
|
|
150
170
|
tests/ pytest suite; tests/sit/phase3/ is the CLI-driven system test
|
|
@@ -156,6 +176,7 @@ tests/ pytest suite; tests/sit/phase3/ is the CLI-driven system test
|
|
|
156
176
|
- [docs/PROTOCOL.md](docs/PROTOCOL.md): the normative format, filing rules, trust computation and compile semantics, for implementers in any language.
|
|
157
177
|
- [docs/RESEARCH-SYNTHESIS.md](docs/RESEARCH-SYNTHESIS.md): the nine sources, what was taken from each, and what none of them had.
|
|
158
178
|
- [docs/INTEGRATION.md](docs/INTEGRATION.md): setup per harness, the local verifier lane, the contract loop.
|
|
179
|
+
- [docs/COMMS-BUS-PLAN.md](docs/COMMS-BUS-PLAN.md): how agents talk through the ledger without spending tokens while they wait, and [docs/RUNBOOK-RELAY.md](docs/RUNBOOK-RELAY.md) to run a relay per agent.
|
|
159
180
|
- [docs/LANDSCAPE.md](docs/LANDSCAPE.md): the competing systems, their verified licenses, the standards receipts align with, and what none of them do.
|
|
160
181
|
- [docs/UAT-PLAN.md](docs/UAT-PLAN.md): the one-week acceptance test across Claude Code, Codex, Antigravity and Grok.
|
|
161
182
|
- [docs/UAT-SELF.md](docs/UAT-SELF.md): the same test for one person with two subscriptions, Claude Code and Codex CLI, with `scripts/uat/` to set it up and to run the cross-lineage verification.
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
judgment_calibration; docs/PHASE3-RELIABILITY.md)
|
|
15
15
|
- Which records carry a signature, and does every signature check out? (signatures;
|
|
16
16
|
a signature says who filed a record, never that it is true)
|
|
17
|
+
- Which lineage labels are only labels: records filed unsigned by an author who holds a
|
|
18
|
+
registered key, and verifications by an identity with no key at all?
|
|
17
19
|
- How much of memory is human-authored vs agent-authored?
|
|
18
20
|
- Which contracts are open, and do their claims meet the bar?
|
|
19
21
|
- What did each agent session actually get shown (compilation receipts)?
|
|
@@ -28,14 +30,25 @@ from . import evidence as ev
|
|
|
28
30
|
from . import reliability as rel
|
|
29
31
|
from . import signing
|
|
30
32
|
from .contracts import status_of
|
|
31
|
-
from .records import is_redacted
|
|
33
|
+
from .records import is_redacted, receipt_visible_to
|
|
32
34
|
from .trust import TIERS, assess, lineage_differs, summarize
|
|
33
35
|
|
|
34
36
|
# Anything a compiled view would have to render on its own line. The renderer collapses
|
|
35
37
|
# these (review S5); the audit names the records that carried them, because a statement
|
|
36
|
-
# shaped like a document is a statement written to be read as one.
|
|
38
|
+
# shaped like a document is a statement written to be read as one. The author block is on
|
|
39
|
+
# that list too: the attribution a view writes is built from the harness, the model and the
|
|
40
|
+
# user a record carries, so a newline there forges a heading as surely as one in a
|
|
41
|
+
# statement, and those come from the environment of whatever filed it (review E-1).
|
|
37
42
|
_CONTROL_RE = re.compile(r"[\x00-\x1f\x7f]")
|
|
38
|
-
_FORMATTED_FIELDS = ("statement", "rationale", "example"
|
|
43
|
+
_FORMATTED_FIELDS = ("statement", "rationale", "example",
|
|
44
|
+
"author.harness", "author.model", "author.user", "author.vendor")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _formatted_value(rec: dict, field: str):
|
|
48
|
+
"""The value ``field`` names: a body key, or an author key written ``author.<key>``."""
|
|
49
|
+
if field.startswith("author."):
|
|
50
|
+
return (rec.get("author") or {}).get(field.split(".", 1)[1])
|
|
51
|
+
return (rec.get("body") or {}).get(field)
|
|
39
52
|
|
|
40
53
|
|
|
41
54
|
def _formatting_problem(text: str) -> Optional[str]:
|
|
@@ -67,10 +80,55 @@ def _same_session_other_harness(claim_author: dict, verifier: dict) -> bool:
|
|
|
67
80
|
)
|
|
68
81
|
|
|
69
82
|
|
|
83
|
+
# the kinds whose author block is a lineage label somebody acts on: what a claim says,
|
|
84
|
+
# whether it was verified, whether a contract moved, whether a claim was withdrawn
|
|
85
|
+
_IDENTITY_KINDS = ("claim", "verification", "contract_update", "retraction")
|
|
86
|
+
# what one section lists before it says "... and N more"
|
|
87
|
+
_SECTION_CAP = 50
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _reader_sees(ledger, rec: dict, by_id: Dict[str, dict], reader_user: Optional[str]) -> bool:
|
|
91
|
+
"""Whether this reader is shown ``rec`` and everything it is about.
|
|
92
|
+
|
|
93
|
+
The record's own scope decides first; a local audit with no reader named sees
|
|
94
|
+
everything of its own. Then what the record names: a verification and a retraction
|
|
95
|
+
name a claim, a contract_update names its contract and the claims it moves, and
|
|
96
|
+
naming somebody's private claim is naming it whatever record the citation sits in. A
|
|
97
|
+
claim this report is not carrying at all -- private to another reader, or redacted --
|
|
98
|
+
takes the record citing it with it, reader or no reader, which is the rule the
|
|
99
|
+
lineage matrix already follows.
|
|
100
|
+
"""
|
|
101
|
+
if reader_user is not None and not receipt_visible_to(rec, reader_user):
|
|
102
|
+
return False
|
|
103
|
+
body = rec.get("body") or {}
|
|
104
|
+
for target in (body.get("claim"), body.get("contract"), *(body.get("claims") or [])):
|
|
105
|
+
if not isinstance(target, str) or target in by_id:
|
|
106
|
+
continue
|
|
107
|
+
cited = ledger.get(target)
|
|
108
|
+
if cited is None or is_redacted(cited) or cited["kind"] == "claim":
|
|
109
|
+
return False # gone, erased, or a claim this report is holding back
|
|
110
|
+
if reader_user is not None and not receipt_visible_to(cited, reader_user):
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _capped(items: List[dict]) -> dict:
|
|
116
|
+
"""{total, items}: the newest ``_SECTION_CAP`` entries and how many there are in all."""
|
|
117
|
+
newest = sorted(items, key=lambda r: (r["ts"], r["id"]), reverse=True)
|
|
118
|
+
return {"total": len(items), "items": newest[:_SECTION_CAP]}
|
|
119
|
+
|
|
120
|
+
|
|
70
121
|
def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Optional[float] = 300,
|
|
71
|
-
path_map=None) -> dict:
|
|
122
|
+
path_map=None, reader_user: Optional[str] = None) -> dict:
|
|
123
|
+
"""The audit, for one reader. ``reader_user`` is who is asking: a user-scoped claim is
|
|
124
|
+
one person's private memory and is listed for its owner and for nobody else, the same
|
|
125
|
+
rule ``receipt_visible_to`` applies everywhere a record is shown (verification review
|
|
126
|
+
F1: the audit listed every user's private statements to any token). A local audit
|
|
127
|
+
with no reader named sees everything, as the local ledger's owner does."""
|
|
72
128
|
assessment = assess(ledger)
|
|
73
129
|
claims = ledger.claims()
|
|
130
|
+
if reader_user is not None:
|
|
131
|
+
claims = [c for c in claims if receipt_visible_to(c, reader_user)]
|
|
74
132
|
by_id = {c["id"]: c for c in claims}
|
|
75
133
|
rep: Dict = {
|
|
76
134
|
"project": ledger.config.get("project"),
|
|
@@ -91,8 +149,8 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
|
|
|
91
149
|
"same_session_different_harness": [],
|
|
92
150
|
"formatted_statements": [],
|
|
93
151
|
"redactions": {"count": 0, "ids": []},
|
|
94
|
-
# absent and empty mean
|
|
95
|
-
# empty list
|
|
152
|
+
# absent and empty mean different things (an absent key is the starter default
|
|
153
|
+
# list, an empty list is none), so the report carries both facts
|
|
96
154
|
"recheck_allow_set": "recheck_allow" in ledger.config,
|
|
97
155
|
"recheck_allow": list(ledger.config.get("recheck_allow") or []),
|
|
98
156
|
"lineage_matrix": {},
|
|
@@ -105,6 +163,8 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
|
|
|
105
163
|
"contracts": [],
|
|
106
164
|
"compilations": [],
|
|
107
165
|
"signatures": {},
|
|
166
|
+
"unsigned_by_registered_signers": {},
|
|
167
|
+
"verifications_by_unregistered_identities": {},
|
|
108
168
|
"recheck": [],
|
|
109
169
|
}
|
|
110
170
|
# measured history, computed from the ledger alone: counts, intervals, and the word
|
|
@@ -135,8 +195,11 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
|
|
|
135
195
|
if backed in ("static", "attested"):
|
|
136
196
|
# a clean static gate is not evidence that the thing runs
|
|
137
197
|
rep["static_only_outcomes"].append({**entry, "assurance": backed})
|
|
138
|
-
|
|
139
|
-
|
|
198
|
+
# a measure, not a tier: every claim whose only verifiers are agents of its own
|
|
199
|
+
# vendor, whether the ledger's policy lets that reach `verified` or not
|
|
200
|
+
if info["verified_by"] and not info.get("cross_vendor") and not info.get("self_verified_by_author") \
|
|
201
|
+
and info["tier"] in ("verified", "self-verified"):
|
|
202
|
+
rep["same_vendor_only"].append({**entry, "verified_by": info["verified_by"], "tier": info["tier"]})
|
|
140
203
|
if info.get("self_verified_by_author"):
|
|
141
204
|
rep["self_verified_by_author"].append({**entry, "verified_by": info["verified_by"]})
|
|
142
205
|
if info["tier"] == "refuted":
|
|
@@ -151,7 +214,7 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
|
|
|
151
214
|
if info["tier"] == "asserted" and b["type"] == "fact":
|
|
152
215
|
rep["asserted_facts"].append(entry)
|
|
153
216
|
for field in _FORMATTED_FIELDS:
|
|
154
|
-
value =
|
|
217
|
+
value = _formatted_value(c, field)
|
|
155
218
|
problem = _formatting_problem(value) if isinstance(value, str) else None
|
|
156
219
|
if problem:
|
|
157
220
|
rep["formatted_statements"].append({**entry, "field": field, "line": problem})
|
|
@@ -230,6 +293,50 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
|
|
|
230
293
|
"invalid_items": [i for i in sig["items"] if i["status"] != "valid"],
|
|
231
294
|
"by_author": dict(attested_by_author),
|
|
232
295
|
}
|
|
296
|
+
# what a lineage label is worth. Outside ``serve``'s token path every field of the
|
|
297
|
+
# author block is whatever the filing process said it was (§8 of docs/PROTOCOL.md), and
|
|
298
|
+
# a signature is the only thing in this ledger that ties one of those labels to a key
|
|
299
|
+
# somebody holds. So the audit names the two places the label is only a label: a record
|
|
300
|
+
# filed unsigned by a user who *has* a registered key, and a verification filed by an
|
|
301
|
+
# identity with no key in the registry at all. Neither is a finding; both are the
|
|
302
|
+
# question to ask before the vendor-lineage sections above are read as facts.
|
|
303
|
+
registry = signing.signers(ledger)
|
|
304
|
+
signer_users = {e.get("user") for e in registry.values() if e.get("user")}
|
|
305
|
+
can_verify = signing.available()
|
|
306
|
+
# the subset of the signatures already verified above that ties a record to a key
|
|
307
|
+
# registered to its *own* author: a valid countersignature by somebody else says that
|
|
308
|
+
# a second party saw the record, never that the author label on it is theirs. Every
|
|
309
|
+
# signature in the ledger is checked once, here and in the signatures block.
|
|
310
|
+
author_signed = signing.author_attested_records(ledger, sig["items"])
|
|
311
|
+
unsigned: List[dict] = []
|
|
312
|
+
unregistered: List[dict] = []
|
|
313
|
+
for rec in ledger.all():
|
|
314
|
+
if rec["kind"] not in _IDENTITY_KINDS or not _reader_sees(ledger, rec, by_id, reader_user):
|
|
315
|
+
continue
|
|
316
|
+
# a tombstone's bytes were erased on purpose (review C1): there is nothing left for
|
|
317
|
+
# a signature to cover, which is not the same thing as a record nobody signed
|
|
318
|
+
if is_redacted(rec):
|
|
319
|
+
continue
|
|
320
|
+
a = rec["author"]
|
|
321
|
+
if can_verify and a.get("user") in signer_users and rec["id"] not in author_signed:
|
|
322
|
+
unsigned.append({"id": rec["id"], "kind": rec["kind"], "user": a.get("user"),
|
|
323
|
+
"harness": a.get("harness"), "ts": rec["ts"]})
|
|
324
|
+
if registry and rec["kind"] == "verification" and a.get("user") not in signer_users:
|
|
325
|
+
b = rec["body"]
|
|
326
|
+
unregistered.append({"id": rec["id"], "claim": b.get("claim"), "user": a.get("user"),
|
|
327
|
+
"harness": a.get("harness"), "vendor": a.get("vendor"),
|
|
328
|
+
"verdict": b.get("verdict"), "ts": rec["ts"]})
|
|
329
|
+
rep["unsigned_by_registered_signers"] = _capped(unsigned)
|
|
330
|
+
if not can_verify:
|
|
331
|
+
# the same answer `verify-signatures` gives: on a machine without the extra every
|
|
332
|
+
# signature is unverifiable, so nothing here may be called unsigned
|
|
333
|
+
rep["unsigned_by_registered_signers"]["unavailable"] = True
|
|
334
|
+
rep["unsigned_by_registered_signers"]["detail"] = f"{signing.NO_CRYPTO}; {signing.INSTALL_HINT}"
|
|
335
|
+
rep["verifications_by_unregistered_identities"] = _capped(unregistered)
|
|
336
|
+
if not registry:
|
|
337
|
+
# a ledger that never registered a key is not a ledger where everybody is
|
|
338
|
+
# unregistered: there is no registry to be outside of
|
|
339
|
+
rep["verifications_by_unregistered_identities"]["no_registry"] = True
|
|
233
340
|
if recheck:
|
|
234
341
|
for c in claims:
|
|
235
342
|
info = assessment[c["id"]]
|
|
@@ -255,13 +362,19 @@ def format_report(rep: dict) -> str:
|
|
|
255
362
|
lines.append("authors: " + ", ".join(f"{k}={v}" for k, v in rep["authors"].most_common()))
|
|
256
363
|
lines.append("kind: " + ", ".join(f"{k}={v}" for k, v in rep["human_vs_agent"].items()))
|
|
257
364
|
lines.append("vendors: " + ", ".join(f"{k}={v}" for k, v in rep["vendors"].most_common()))
|
|
365
|
+
unsigned = rep.get("unsigned_by_registered_signers") or {}
|
|
366
|
+
unregistered = rep.get("verifications_by_unregistered_identities") or {}
|
|
367
|
+
lines.append(f"labels: unsigned by a registered signer={unsigned.get('total', 0)}, "
|
|
368
|
+
f"verifications by an unregistered identity={unregistered.get('total', 0)}")
|
|
258
369
|
if not rep.get("trusted", True):
|
|
259
370
|
lines.append("repo: UNTRUSTED -- config.json's policy keys (recheck_allow, "
|
|
260
371
|
"recheck_allow_shell, redact, path_map, org_path, signers, vendor_map, "
|
|
261
|
-
"cross_vendor_required
|
|
372
|
+
"cross_vendor_required, stale_after_days, reliability, max_clock_skew_seconds) "
|
|
373
|
+
"were ignored; run `agent-memory trust .` to honour them")
|
|
262
374
|
if not rep.get("recheck_allow_set"):
|
|
263
|
-
lines.append("
|
|
264
|
-
"
|
|
375
|
+
lines.append("note: config.recheck_allow is not set: the re-check lane is held to the starter "
|
|
376
|
+
"list (test runners and build tools); run `agent-memory init` to write it into "
|
|
377
|
+
"config.json and narrow it to what your receipts actually are")
|
|
265
378
|
elif not rep.get("recheck_allow"):
|
|
266
379
|
# deliberate, and the opposite of the absent case: nothing is re-run here
|
|
267
380
|
lines.append("note: recheck_allow is empty: no receipt will be re-run on this machine")
|
|
@@ -274,6 +387,19 @@ def format_report(rep: dict) -> str:
|
|
|
274
387
|
if len(rows) > 50:
|
|
275
388
|
lines.append(f"- ... {len(rows) - 50} more")
|
|
276
389
|
|
|
390
|
+
def capped(title: str, sec: dict, fmt, note: Optional[str] = None):
|
|
391
|
+
"""A section the report already capped: it carries its own total and items."""
|
|
392
|
+
sec = sec or {}
|
|
393
|
+
items = sec.get("items") or []
|
|
394
|
+
lines.append("")
|
|
395
|
+
lines.append(f"## {title} ({sec.get('total', 0)})")
|
|
396
|
+
if note:
|
|
397
|
+
lines.append(f"- {note}")
|
|
398
|
+
for r in items:
|
|
399
|
+
lines.append("- " + fmt(r))
|
|
400
|
+
if sec.get("total", 0) > len(items):
|
|
401
|
+
lines.append(f"- ... {sec['total'] - len(items)} more")
|
|
402
|
+
|
|
277
403
|
section("Outcome claims with receipts nobody re-ran", rep["unrechecked_outcomes"],
|
|
278
404
|
lambda r: f"{r['id']} [{r['by']}] {r['statement']}")
|
|
279
405
|
section("Outcome claims whose receipts did not succeed", rep["failing_receipt_outcomes"],
|
|
@@ -283,8 +409,8 @@ def format_report(rep: dict) -> str:
|
|
|
283
409
|
section("Receipts whose assurance was declared above the computed default", rep["assurance_overridden"],
|
|
284
410
|
lambda r: f"{r['id']} [{r['by']}] declared {r['declared']}, computed {r['computed']}: {r['what']}"
|
|
285
411
|
+ (f" (cited by {', '.join(r['claims'])})" if r["claims"] else " (cited by no claim)"))
|
|
286
|
-
section("Verified only by the same vendor (
|
|
287
|
-
lambda r: f"{r['id']} [{r['vendor']} verified by {', '.join(r['verified_by'])}] {r['statement']}")
|
|
412
|
+
section("Verified only by the same vendor (no person or other vendor has re-run it)", rep["same_vendor_only"],
|
|
413
|
+
lambda r: f"{r['id']} [{r['vendor']} verified by {', '.join(r['verified_by'])}; {r.get('tier','')}] {r['statement']}")
|
|
288
414
|
section("Verified only by the claim's own author", rep["self_verified_by_author"],
|
|
289
415
|
lambda r: f"{r['id']} [{r['by']}] {r['statement']}")
|
|
290
416
|
section("Verified from the claim's own session under another harness (collusion risk)",
|
|
@@ -320,6 +446,14 @@ def format_report(rep: dict) -> str:
|
|
|
320
446
|
for item in sig.get("invalid_items", [])[:50]:
|
|
321
447
|
lines.append(f"- {item['id']} over {item['record']} [{item['key_id']}]: {item['status']} -- "
|
|
322
448
|
f"{item['detail']}")
|
|
449
|
+
capped("Unsigned records by registered signers", unsigned,
|
|
450
|
+
lambda r: f"{r['id']} [{r['kind']}] {r['user']} via {r['harness']} at {r['ts']}",
|
|
451
|
+
note=unsigned.get("detail") if unsigned.get("unavailable") else None)
|
|
452
|
+
capped("Verifications by identities with no registered key", unregistered,
|
|
453
|
+
lambda r: f"{r['id']} over {r['claim']} [{r['user']} via {r['harness']}, {r['vendor']}] "
|
|
454
|
+
f"{r['verdict']} at {r['ts']}",
|
|
455
|
+
note=("config.signers is empty: this ledger registered no key, so no identity in it "
|
|
456
|
+
"can be checked" if unregistered.get("no_registry") else None))
|
|
323
457
|
lines.append("")
|
|
324
458
|
lines.append(f"## Verifier lineage (refutation rates) ({len(rep['lineage_matrix'])})")
|
|
325
459
|
for key, cell in sorted(rep["lineage_matrix"].items()):
|