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.
Files changed (80) hide show
  1. {strata_agent_memory-0.2.0/strata_agent_memory.egg-info → strata_agent_memory-0.2.2}/PKG-INFO +26 -5
  2. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/README.md +25 -4
  3. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/__init__.py +1 -1
  4. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/audit.py +148 -14
  5. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/cli.py +509 -60
  6. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/compact.py +5 -2
  7. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/compiler.py +88 -3
  8. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/contracts.py +193 -31
  9. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/evidence.py +365 -57
  10. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/export.py +14 -5
  11. strata_agent_memory-0.2.2/agent_memory/hooks.py +1085 -0
  12. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/identity.py +26 -2
  13. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/ledger.py +201 -104
  14. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/mcp_server.py +65 -20
  15. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/recheck.py +14 -3
  16. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/records.py +198 -6
  17. strata_agent_memory-0.2.2/agent_memory/relay/__init__.py +9 -0
  18. strata_agent_memory-0.2.2/agent_memory/relay/adapters.py +521 -0
  19. strata_agent_memory-0.2.2/agent_memory/relay/cli.py +226 -0
  20. strata_agent_memory-0.2.2/agent_memory/relay/client.py +843 -0
  21. strata_agent_memory-0.2.2/agent_memory/relay/core.py +821 -0
  22. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/reliability.py +21 -4
  23. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/remote.py +99 -3
  24. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/render.py +39 -3
  25. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/serve.py +279 -34
  26. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/signing.py +122 -18
  27. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/trust.py +118 -36
  28. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/pyproject.toml +2 -2
  29. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2/strata_agent_memory.egg-info}/PKG-INFO +26 -5
  30. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/SOURCES.txt +11 -0
  31. strata_agent_memory-0.2.2/tests/test_audit.py +485 -0
  32. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_cli.py +65 -0
  33. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_compiler_render.py +192 -1
  34. strata_agent_memory-0.2.2/tests/test_contracts.py +511 -0
  35. strata_agent_memory-0.2.2/tests/test_deploy_relay.py +652 -0
  36. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_deploy_replica.py +210 -2
  37. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_deploy_tokens.py +68 -0
  38. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_evidence.py +72 -19
  39. strata_agent_memory-0.2.2/tests/test_hooks.py +1571 -0
  40. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_identity.py +47 -0
  41. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_ledger.py +58 -12
  42. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_packaging.py +4 -1
  43. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_pathmap.py +10 -2
  44. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_phase2.py +103 -5
  45. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_recheck.py +49 -2
  46. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_records.py +108 -0
  47. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_redaction.py +27 -0
  48. strata_agent_memory-0.2.2/tests/test_relay_client.py +1420 -0
  49. strata_agent_memory-0.2.2/tests/test_relay_codex.py +381 -0
  50. strata_agent_memory-0.2.2/tests/test_relay_core.py +966 -0
  51. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_reliability.py +3 -1
  52. strata_agent_memory-0.2.2/tests/test_remote.py +1217 -0
  53. strata_agent_memory-0.2.2/tests/test_remote_signing.py +205 -0
  54. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_review2_fixes.py +14 -1
  55. strata_agent_memory-0.2.2/tests/test_review3_fixes.py +680 -0
  56. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_scale.py +50 -0
  57. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_serve.py +660 -23
  58. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_trust.py +41 -3
  59. strata_agent_memory-0.2.0/agent_memory/hooks.py +0 -458
  60. strata_agent_memory-0.2.0/tests/test_audit.py +0 -229
  61. strata_agent_memory-0.2.0/tests/test_contracts.py +0 -292
  62. strata_agent_memory-0.2.0/tests/test_hooks.py +0 -556
  63. strata_agent_memory-0.2.0/tests/test_remote.py +0 -525
  64. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/LICENSE +0 -0
  65. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/__main__.py +0 -0
  66. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/memorizer.py +0 -0
  67. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/pathmap.py +0 -0
  68. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/agent_memory/stats.py +0 -0
  69. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/setup.cfg +0 -0
  70. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
  71. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/entry_points.txt +0 -0
  72. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/requires.txt +0 -0
  73. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/strata_agent_memory.egg-info/top_level.txt +0 -0
  74. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_compact.py +0 -0
  75. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_issue_bridge.py +0 -0
  76. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_ledger_index.py +0 -0
  77. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_memorizer.py +0 -0
  78. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_review_fixes.py +0 -0
  79. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_signing.py +0 -0
  80. {strata_agent_memory-0.2.0 → strata_agent_memory-0.2.2}/tests/test_stats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strata-agent-memory
3
- Version: 0.2.0
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
- - **Same-vendor review is not verification.** A Claude reviewer approving a
49
- Claude coder yields `self-verified`. Only a human or a different vendor
50
- lineage (Codex, Gemini, a local Qwen) promotes a claim to `verified`.
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
- - **Same-vendor review is not verification.** A Claude reviewer approving a
25
- Claude coder yields `self-verified`. Only a human or a different vendor
26
- lineage (Codex, Gemini, a local Qwen) promotes a claim to `verified`.
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.
@@ -9,4 +9,4 @@ see, what did it claim, and can the proof be re-run.
9
9
  The core is dependency-free (Python 3.10+ standard library only).
10
10
  """
11
11
 
12
- __version__ = "0.2.0"
12
+ __version__ = "0.2.2"
@@ -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 opposite things (an absent key allows every command, an
95
- # empty list allows none), so the report carries both facts
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
- if info["tier"] == "self-verified":
139
- rep["same_vendor_only"].append({**entry, "verified_by": info["verified_by"]})
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 = b.get(field)
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) were ignored; run `agent-memory trust .` to honour them")
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("warning: config.recheck_allow is not set: the re-check lane will run whatever "
264
- "command a receipt records; run `agent-memory init` to write the starter list")
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 (collusion risk)", rep["same_vendor_only"],
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()):