strata-agent-memory 0.2.7__tar.gz → 0.2.8__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.7/strata_agent_memory.egg-info → strata_agent_memory-0.2.8}/PKG-INFO +1 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/__init__.py +1 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/compiler.py +107 -29
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/contracts.py +2 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/evidence.py +112 -12
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/ledger.py +1 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/mcp_server.py +16 -7
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/remote.py +43 -5
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/render.py +7 -1
- strata_agent_memory-0.2.8/agent_memory/retrieval.py +205 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/serve.py +17 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/README.md +1 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/backup-tokens.sh +4 -2
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/install-replica-driver.sh +104 -71
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/onboard-agent.sh +9 -4
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/upgrade-driver.sh +23 -7
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/pyproject.toml +13 -2
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8/strata_agent_memory.egg-info}/PKG-INFO +1 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/SOURCES.txt +10 -0
- strata_agent_memory-0.2.8/tests/test_binding_refusal_97e36882.py +122 -0
- strata_agent_memory-0.2.8/tests/test_build_recheck_image_e4a93fd6.py +51 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_deploy_mac.py +16 -0
- strata_agent_memory-0.2.8/tests/test_memoryagentbench_60a8abfc.py +374 -0
- strata_agent_memory-0.2.8/tests/test_memsyco_7b16e8e4.py +553 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_packaging.py +2 -0
- strata_agent_memory-0.2.8/tests/test_proof_exclusions_e4a93fd6.py +80 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_recheck_sandbox_543e48a7.py +24 -2
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_relay_client.py +1 -1
- strata_agent_memory-0.2.8/tests/test_reply_cap_cfbd7681.py +229 -0
- strata_agent_memory-0.2.8/tests/test_retrieval_plugin_9e166c2f.py +364 -0
- strata_agent_memory-0.2.8/tests/test_sandbox_image_e4a93fd6.py +320 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_served_ledger_6e8e49f3.py +4 -0
- strata_agent_memory-0.2.8/tests/test_tools_clone_4b72f0c1.py +232 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/LICENSE +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/README.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/__main__.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/approvals.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/audit.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/cli.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/compact.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/export.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/hooks.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/identity.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/instances.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/kit.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/lane.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/measure.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/memorizer.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/pathmap.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/recheck.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/records.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/relay/__init__.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/relay/adapters.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/relay/cli.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/relay/client.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/relay/core.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/reliability.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/roster.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/signing.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/stats.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/supersession.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/trust.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/agent_memory/wake.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/lib/served-ledger.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/agent-keys.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/agent-session.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/install-hooks-mac.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/install-relay-driver.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/install-runner-driver.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/instances.example.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/relay-agent.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/seed-board.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/seed-mail-context.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/mac/seed-topics.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/.claude-plugin/marketplace.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/README.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/bin/agent-memory-approvals +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/bin/agent-memory-wake +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/hooks/hooks.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/monitors/monitors.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/rules/approvals.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/approvals/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/approve/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/inbox/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/reject/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-answer/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-answer/agents/openai.yaml +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/agents/openai.yaml +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/rules/approvals.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/scripts/agent-memory-approvals +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/relay/README.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/relay/agent-memory-relay@.service +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/relay/install-relay-mac.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/relay/install-relay.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/relay/macos/com.strata.agent-memory.relay.plist.template +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/deploy/relay/relay-launch.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/setup.cfg +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/entry_points.txt +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/requires.txt +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/top_level.txt +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_assessment_67b26eae.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_audit.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_audit_lane_numbers_ea89ba26.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_audit_measurement_13190a20.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_backlog_check.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_changelog_fragments.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_changelog_guard_shallow_57a2f78e.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_ci_docs_only_353ce010.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_cli.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_compact.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_compiler.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_compiler_render.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_config.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_contract_amend_649e0b45.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_contracts.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_deploy_relay.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_deploy_replica.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_deploy_tokens.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_evidence.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_find_root_b4b5019b.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_hooks.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_identity.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_install_lane_cb430a30.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_issue_bridge.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_lane_63b7cf8b.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_lane_derived_roots_817ab446.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_lane_nightly_84b22b63.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_lane_priority_779d51ba.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_lane_stop_63b7cf8b.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_ledger.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_ledger_index.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_live_wake_c217c336.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_local_lanes_sibling_1411a2b2.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_memorizer.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_outcome_commit_binding_b1de564d.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_outcome_needs_evidence_b1de564d.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_pathmap.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_phase2.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_plugins.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_receipt_reports_274e6199.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_recheck.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_recheck_away_207af145.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_recheck_remote_8185d2d3.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_recheck_sandbox_nits_543e48a7.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_records.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_redaction.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_relay_adapters.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_relay_codex.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_relay_core.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_relay_instance_2a2e3887.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_relay_release_526f5261.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_relay_release_gate_b3b3e487.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_reliability.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_remote.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_remote_signing.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_review2_fixes.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_review3_fixes.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_review_fixes.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_rollback_78710ebf.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_scale.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_serve.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_signing.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_sit_fleet_1b24b0b1.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_sit_fleet_ci_0d9b76a2.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_sit_var_bindings_e802ca84.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_stats.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_supersedes_61416280.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_trust.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.8}/tests/test_uv_lock_env_274e6199.py +0 -0
{strata_agent_memory-0.2.7/strata_agent_memory.egg-info → strata_agent_memory-0.2.8}/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.8
|
|
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
|
|
@@ -14,6 +14,7 @@ Roles and modes follow the fork/isolate rule:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import re
|
|
17
|
+
import sys
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from datetime import timedelta
|
|
19
20
|
from typing import Callable, Dict, Iterable, List, Optional, Tuple
|
|
@@ -142,6 +143,9 @@ class Compiled:
|
|
|
142
143
|
pinned_cap: Optional[int] = None
|
|
143
144
|
pinned_cut: Dict[str, int] = field(default_factory=dict)
|
|
144
145
|
pinned_full: Dict[str, int] = field(default_factory=dict)
|
|
146
|
+
# the retrieval plugin that took part (ctr_9e166c2f291b829b): {plugin, share, ids}, and
|
|
147
|
+
# error when it could not run; None when config.retrieval names none
|
|
148
|
+
retrieval: Optional[Dict] = None
|
|
145
149
|
|
|
146
150
|
def included_ids(self) -> List[str]:
|
|
147
151
|
return [i.id for items in self.sections.values() for i in items]
|
|
@@ -590,6 +594,10 @@ def compile_context(
|
|
|
590
594
|
else:
|
|
591
595
|
out.sections["working"].append(item)
|
|
592
596
|
|
|
597
|
+
# the retrieval plugin (ctr_9e166c2f291b829b): the ids it finds for the task, of which
|
|
598
|
+
# the reader sees only what this compile's own policy filter let through (candidates)
|
|
599
|
+
retrieval_cfg, retrieved = _retrieval(ledger, task, {item.id: item for item, _ in candidates}, out, assessment)
|
|
600
|
+
|
|
593
601
|
seen_dnr = set()
|
|
594
602
|
for it in do_not_retry:
|
|
595
603
|
key = (it.id, it.statement)
|
|
@@ -619,35 +627,72 @@ def compile_context(
|
|
|
619
627
|
# costed provisionally here and the difference is charged once it is known
|
|
620
628
|
provisional = _obligations(role, mode, round_no, bool(out.sections["cross_check"]), bool(contract_id), lens)
|
|
621
629
|
overhead = overhead_tokens(out, provisional)
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
for
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
used
|
|
649
|
-
|
|
650
|
-
|
|
630
|
+
def allocate(reserve: int, found: List[Item], fixed: Optional[List[Item]] = None):
|
|
631
|
+
"""(kept, used, left out for the budget): pass one fills each section up to its
|
|
632
|
+
share, the retrieval plugin's ``found`` fill its section up to ``reserve``, and pass
|
|
633
|
+
two fills leftovers in priority order. ``fixed`` is the constraints section as
|
|
634
|
+
compile alone keeps it: the plugin's share is never taken from what always applies
|
|
635
|
+
(the review of ctr_9e166c2f291b829b)."""
|
|
636
|
+
used = overhead
|
|
637
|
+
kept: Dict[str, List[Item]] = {name: [] for name in SECTION_ORDER}
|
|
638
|
+
remaining_by_section: Dict[str, List[Item]] = {name: [] for name in SECTION_ORDER}
|
|
639
|
+
# pass 1: each section up to its share; pass 2: fill leftovers in priority order
|
|
640
|
+
for name in SECTION_ORDER:
|
|
641
|
+
if name == "constraints" and fixed is not None:
|
|
642
|
+
kept[name] = list(fixed)
|
|
643
|
+
used += sum(cost_of(item) for item in fixed)
|
|
644
|
+
held = {id(item) for item in fixed}
|
|
645
|
+
remaining_by_section[name] = [item for item in out.sections[name] if id(item) not in held]
|
|
646
|
+
continue
|
|
647
|
+
cap = int(budget * SECTION_SHARE[name])
|
|
648
|
+
# half of the constraints section is kept for its human-filed items, which arrive
|
|
649
|
+
# in standing order, so the freshly filed ones compete for the other half only
|
|
650
|
+
reserved = int(cap * CONSTRAINT_HUMAN_RESERVE) if name == "constraints" else 0
|
|
651
|
+
section_used = unreserved_used = 0
|
|
652
|
+
for item in out.sections[name]:
|
|
653
|
+
cost = cost_of(item)
|
|
654
|
+
reserve_eligible = bool(reserved) and item.is_human
|
|
655
|
+
fits_share = reserve_eligible or unreserved_used + cost <= cap - reserved
|
|
656
|
+
if section_used + cost <= cap and fits_share and used + cost <= budget - reserve:
|
|
657
|
+
kept[name].append(item)
|
|
658
|
+
section_used += cost
|
|
659
|
+
unreserved_used += 0 if reserve_eligible else cost
|
|
660
|
+
used += cost
|
|
661
|
+
else:
|
|
662
|
+
remaining_by_section[name].append(item)
|
|
663
|
+
taken = {item.id for items in kept.values() for item in items}
|
|
664
|
+
if found:
|
|
665
|
+
# what the plugin found and pass 1 did not already keep, in the plugin's order,
|
|
666
|
+
# up to its share: a record shown once, in the section that placed it first
|
|
667
|
+
kept["retrieved"], spent = [], 0
|
|
668
|
+
for item in found:
|
|
669
|
+
cost = cost_of(item)
|
|
670
|
+
if item.id not in taken and spent + cost <= reserve and used + cost <= budget:
|
|
671
|
+
kept["retrieved"].append(item)
|
|
672
|
+
taken.add(item.id)
|
|
673
|
+
spent += cost
|
|
674
|
+
used += cost
|
|
675
|
+
left_out: List[str] = []
|
|
676
|
+
for name in SECTION_ORDER:
|
|
677
|
+
for item in remaining_by_section[name]:
|
|
678
|
+
if item.id in taken:
|
|
679
|
+
continue # the retrieved section placed it
|
|
680
|
+
cost = cost_of(item)
|
|
681
|
+
if used + cost <= budget:
|
|
682
|
+
kept[name].append(item)
|
|
683
|
+
used += cost
|
|
684
|
+
else:
|
|
685
|
+
left_out.append(item.id)
|
|
686
|
+
return kept, used, left_out
|
|
687
|
+
|
|
688
|
+
# compile alone; and when the retrieval plugin found something, again with its share
|
|
689
|
+
# held back from every section but the constraints, which keep what compile alone
|
|
690
|
+
# gives them. A plugin that finds nothing changes nothing (BENCHMARK-PLAN 1.2)
|
|
691
|
+
kept, used, left_out = allocate(0, [])
|
|
692
|
+
if retrieved:
|
|
693
|
+
kept, used, left_out = allocate(int(budget * retrieval_cfg["share"]), retrieved, fixed=kept["constraints"])
|
|
694
|
+
out.retrieval["ids"] = [item.id for item in kept["retrieved"]]
|
|
695
|
+
out.excluded += [(rid, "budget") for rid in left_out]
|
|
651
696
|
out.sections = kept
|
|
652
697
|
out.used_tokens = used
|
|
653
698
|
|
|
@@ -668,6 +713,34 @@ def compile_context(
|
|
|
668
713
|
return out
|
|
669
714
|
|
|
670
715
|
|
|
716
|
+
def _retrieval(ledger, task: Optional[str], eligible: Dict[str, "Item"], out: "Compiled",
|
|
717
|
+
assessment: Optional[dict] = None):
|
|
718
|
+
"""(the plugin's settings or None, the eligible items it found, best first), and
|
|
719
|
+
``out.retrieval`` set to what the receipt records. A plugin that cannot run leaves the
|
|
720
|
+
view as compile alone makes it, and says so: on the receipt, and in one stderr line."""
|
|
721
|
+
from . import retrieval as retrieval_mod # local: retrieval imports this module
|
|
722
|
+
try:
|
|
723
|
+
cfg = retrieval_mod.settings(ledger.config)
|
|
724
|
+
except retrieval_mod.RetrievalError as exc:
|
|
725
|
+
out.retrieval = {"plugin": retrieval_mod.configured_name(ledger.config), "share": None, "ids": [],
|
|
726
|
+
"error": str(exc)}
|
|
727
|
+
print(f"agent-memory: the retrieval plugin did not run ({exc}); compiled without it", file=sys.stderr)
|
|
728
|
+
return None, []
|
|
729
|
+
if cfg is None:
|
|
730
|
+
return None, []
|
|
731
|
+
out.retrieval = {"plugin": cfg["plugin"], "share": cfg["share"], "ids": []}
|
|
732
|
+
if not task:
|
|
733
|
+
return cfg, [] # nothing to search by: a view with no task is compile's alone
|
|
734
|
+
try:
|
|
735
|
+
found = retrieval_mod.retrieve(ledger, task, cfg, assessment)
|
|
736
|
+
except retrieval_mod.RetrievalError as exc:
|
|
737
|
+
out.retrieval["error"] = str(exc)
|
|
738
|
+
print(f"agent-memory: the retrieval plugin {cfg['plugin']} did not run ({exc}); compiled without it",
|
|
739
|
+
file=sys.stderr)
|
|
740
|
+
return cfg, []
|
|
741
|
+
return cfg, [eligible[rid] for rid in found if rid in eligible]
|
|
742
|
+
|
|
743
|
+
|
|
671
744
|
def compiled_from_json(payload: dict) -> Compiled:
|
|
672
745
|
"""Rebuild a Compiled from ``render.render_json``'s payload.
|
|
673
746
|
|
|
@@ -690,6 +763,7 @@ def compiled_from_json(payload: dict) -> Compiled:
|
|
|
690
763
|
# saying it was cut
|
|
691
764
|
board=[dict(row) for row in payload.get("board") or []],
|
|
692
765
|
)
|
|
766
|
+
out.retrieval = payload.get("retrieval") if isinstance(payload.get("retrieval"), dict) else None
|
|
693
767
|
display = payload.get("reader_display")
|
|
694
768
|
out.reader_display = display if isinstance(display, str) and display else None
|
|
695
769
|
out.config_notes = [n for n in payload.get("config_notes") or [] if isinstance(n, str)]
|
|
@@ -840,6 +914,10 @@ def record_compilation(ledger, compiled: Compiled, rendered: str, render_name: s
|
|
|
840
914
|
# which reliability numbers the reader was shown, or null when annotation was off
|
|
841
915
|
"reliability": compiled.reliability_meta,
|
|
842
916
|
}
|
|
917
|
+
if compiled.retrieval is not None:
|
|
918
|
+
# which configuration produced the view (ctr_9e166c2f291b829b): the plugin, its
|
|
919
|
+
# share, the ids it added, and why it did not run when it did not
|
|
920
|
+
body["retrieval"] = compiled.retrieval
|
|
843
921
|
# which repository the view was cut to, and where it was served and asked from: set
|
|
844
922
|
# only when they are, so a plain view's receipt is what it always was
|
|
845
923
|
for key, value in (("repo", compiled.repo), ("served", compiled.served or None),
|
|
@@ -765,7 +765,8 @@ def run_acceptance(ledger, contract_id: str, cwd: Optional[str] = None, timeout:
|
|
|
765
765
|
detail = (f"exit {body['exit']} (expected {expect_exit})" + (f"; {body.get('summary')}" if body.get("summary") else "")
|
|
766
766
|
+ ("; timed out, and everything it started was stopped" if body.get("timed_out") else ""))
|
|
767
767
|
if sandbox is not None:
|
|
768
|
-
reason = ev.sandbox_failure(sandbox["image"], body, expect_exit, check["cmd"]
|
|
768
|
+
reason = (ev.sandbox_failure(sandbox["image"], body, expect_exit, check["cmd"],
|
|
769
|
+
unproven=ev.image_proof(sandbox, where)) if not ok else None)
|
|
769
770
|
if reason:
|
|
770
771
|
outcome, detail = "error", f"{reason}; {detail}"
|
|
771
772
|
detail = f"sandboxed: podman {sandbox['image']}; {detail}"
|
|
@@ -1073,6 +1073,14 @@ def resolved_directory(path, what: str) -> str:
|
|
|
1073
1073
|
# cannot be had here -- no podman, no image, a directory that is not a worktree, a malformed
|
|
1074
1074
|
# key -- is an `error`, never a run on the host.
|
|
1075
1075
|
SANDBOX_IMAGE = "am-recheck:0.2" # deploy/driver/Containerfile.recheck, built once on the driver
|
|
1076
|
+
# what an image deploy/driver/build-recheck-image.sh made says of itself: the sha256 of the
|
|
1077
|
+
# lockfile the whole suite passed against inside it, and how many tests the build left out
|
|
1078
|
+
# by a named marker. Only that script sets them, and only after its proof run is green.
|
|
1079
|
+
PROVEN_LABEL = "io.agent-memory.recheck.proven"
|
|
1080
|
+
EXCLUDED_LABEL = "io.agent-memory.recheck.excluded"
|
|
1081
|
+
SANDBOX_LOCKFILE = "uv.lock"
|
|
1082
|
+
PROOF_CACHE_SECONDS = 300.0 # a resident lane sees an image rebuilt under it within this
|
|
1083
|
+
_PROOF_CACHE: Dict[Tuple[str, str], Tuple[float, str]] = {}
|
|
1076
1084
|
SANDBOX_HOST_ENV = ("LANG", "TZ")
|
|
1077
1085
|
_SANDBOX_DEFAULTS = {"memory": "2g", "cpus": 2, "pids": 512}
|
|
1078
1086
|
_IMAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/:@-]*$")
|
|
@@ -1100,15 +1108,18 @@ class SandboxError(ValueError):
|
|
|
1100
1108
|
|
|
1101
1109
|
def sandbox_config(policy: Optional[dict]) -> Optional[dict]:
|
|
1102
1110
|
"""The sandbox ``policy`` asks a re-check to run in, or None for none. ``recheck_sandbox``
|
|
1103
|
-
is true or ``{engine: podman, image, memory, cpus, pids}``; anything else raises
|
|
1104
|
-
SandboxError, because an operator who asked for a sandbox gets one or no re-run.
|
|
1111
|
+
is true or ``{engine: podman, image, memory, cpus, pids, lockfile}``; anything else raises
|
|
1112
|
+
SandboxError, because an operator who asked for a sandbox gets one or no re-run.
|
|
1113
|
+
``lockfile`` names the file, relative to a worktree's top, the image is proven against
|
|
1114
|
+
(image_proof); uv.lock unless it says otherwise."""
|
|
1105
1115
|
value = (policy or {}).get("recheck_sandbox")
|
|
1106
1116
|
if value is None or value is False:
|
|
1107
1117
|
return None
|
|
1108
1118
|
if value is True:
|
|
1109
1119
|
value = {}
|
|
1110
1120
|
if not isinstance(value, dict):
|
|
1111
|
-
raise SandboxError("recheck_sandbox must be true or an object
|
|
1121
|
+
raise SandboxError("recheck_sandbox must be true or an object "
|
|
1122
|
+
"{engine: podman, image, memory, cpus, pids, lockfile}")
|
|
1112
1123
|
engine = value.get("engine", "podman")
|
|
1113
1124
|
if engine != "podman":
|
|
1114
1125
|
raise SandboxError(f"recheck_sandbox.engine {engine!r}: podman is the only engine")
|
|
@@ -1116,6 +1127,7 @@ def sandbox_config(policy: Optional[dict]) -> Optional[dict]:
|
|
|
1116
1127
|
memory = value.get("memory", _SANDBOX_DEFAULTS["memory"])
|
|
1117
1128
|
cpus = value.get("cpus", _SANDBOX_DEFAULTS["cpus"])
|
|
1118
1129
|
pids = value.get("pids", _SANDBOX_DEFAULTS["pids"])
|
|
1130
|
+
lockfile = value.get("lockfile", SANDBOX_LOCKFILE)
|
|
1119
1131
|
if not isinstance(image, str) or not _IMAGE_RE.match(image):
|
|
1120
1132
|
raise SandboxError(f"recheck_sandbox.image {image!r} is not an image reference")
|
|
1121
1133
|
if not isinstance(memory, str) or not _MEMORY_RE.match(memory):
|
|
@@ -1124,7 +1136,74 @@ def sandbox_config(policy: Optional[dict]) -> Optional[dict]:
|
|
|
1124
1136
|
raise SandboxError(f"recheck_sandbox.cpus {cpus!r} is not a number of cpus")
|
|
1125
1137
|
if isinstance(pids, bool) or not isinstance(pids, int) or not 0 < pids <= 1_000_000:
|
|
1126
1138
|
raise SandboxError(f"recheck_sandbox.pids {pids!r} is not a process count")
|
|
1127
|
-
|
|
1139
|
+
if not isinstance(lockfile, str) or not lockfile or PurePosixPath(lockfile).is_absolute() \
|
|
1140
|
+
or ".." in PurePosixPath(lockfile).parts:
|
|
1141
|
+
raise SandboxError(f"recheck_sandbox.lockfile {lockfile!r} is not a path inside a worktree")
|
|
1142
|
+
return {"engine": "podman", "image": image, "memory": memory, "cpus": cpus, "pids": pids,
|
|
1143
|
+
"lockfile": lockfile}
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def image_proof(sandbox: dict, cwd: str, podman: Optional[str] = None) -> Optional[str]:
|
|
1147
|
+
"""None when the sandbox's image is proven complete for a re-run in ``cwd``, else why
|
|
1148
|
+
it is not: no lockfile there, an image with no proof, or one proven for another
|
|
1149
|
+
lockfile. Proven means build-recheck-image.sh ran the whole suite inside the image with
|
|
1150
|
+
a tree's top mounted at /work, against that top's lockfile, and labelled the image with
|
|
1151
|
+
the lockfile's sha256 (PROVEN_LABEL). So only a re-run whose own directory is such a top
|
|
1152
|
+
is what was proven: a container mounts nothing but the re-run's directory, and one below
|
|
1153
|
+
the top sees neither its lockfile nor the package the proof imported from /work (the
|
|
1154
|
+
review of 9a9cbd6). Until then a failure in the sandbox may be the image's and not the
|
|
1155
|
+
code's, so it refutes nothing (sandbox_failure; ctr_e4a93fd6d32e2b66)."""
|
|
1156
|
+
name = sandbox.get("lockfile") or SANDBOX_LOCKFILE
|
|
1157
|
+
lockfile = Path(cwd).resolve() / name
|
|
1158
|
+
if not lockfile.is_file():
|
|
1159
|
+
above = _worktree_file(Path(cwd), name)
|
|
1160
|
+
if above is not None:
|
|
1161
|
+
return (f"the re-run's directory is below its tree's top ({above.parent}), and the image is "
|
|
1162
|
+
f"proven only with a top mounted at /work")
|
|
1163
|
+
return f"the worktree holds no {name}"
|
|
1164
|
+
try:
|
|
1165
|
+
digest = hashlib.sha256(lockfile.read_bytes()).hexdigest()
|
|
1166
|
+
except OSError as exc:
|
|
1167
|
+
return f"{lockfile} cannot be read: {exc}"
|
|
1168
|
+
image = sandbox["image"]
|
|
1169
|
+
proven = _image_label(podman or shutil.which("podman") or "podman", image, PROVEN_LABEL)
|
|
1170
|
+
if not proven:
|
|
1171
|
+
return f"{image} carries no proof"
|
|
1172
|
+
if proven != digest:
|
|
1173
|
+
return f"{image} is proven for lockfile {proven[:12]}, and this worktree's is {digest[:12]}"
|
|
1174
|
+
return None
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def _worktree_file(start: Path, name: str) -> Optional[Path]:
|
|
1178
|
+
"""``name`` in ``start`` or the nearest directory above it, stopping at the worktree's
|
|
1179
|
+
top (the first directory holding .git): a receipt's cwd is often a subdirectory."""
|
|
1180
|
+
here = start.resolve()
|
|
1181
|
+
while True:
|
|
1182
|
+
candidate = here / name
|
|
1183
|
+
if candidate.is_file():
|
|
1184
|
+
return candidate
|
|
1185
|
+
if (here / ".git").exists() or here.parent == here:
|
|
1186
|
+
return None
|
|
1187
|
+
here = here.parent
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _image_label(podman: str, image: str, label: str) -> str:
|
|
1191
|
+
"""One label of a local image, '' when it has none or podman cannot say; asked at most
|
|
1192
|
+
once per PROOF_CACHE_SECONDS per image, since a lane re-runs many receipts in one."""
|
|
1193
|
+
now = time.monotonic()
|
|
1194
|
+
cached = _PROOF_CACHE.get((image, label))
|
|
1195
|
+
if cached is not None and now - cached[0] < PROOF_CACHE_SECONDS:
|
|
1196
|
+
return cached[1]
|
|
1197
|
+
try:
|
|
1198
|
+
done = subprocess.run([podman, "image", "inspect", "--format",
|
|
1199
|
+
"{{ index .Config.Labels \"%s\" }}" % label, image],
|
|
1200
|
+
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30)
|
|
1201
|
+
value = done.stdout.strip() if done.returncode == 0 else ""
|
|
1202
|
+
except (OSError, subprocess.SubprocessError):
|
|
1203
|
+
value = ""
|
|
1204
|
+
value = "" if value in ("<no value>", "<nil>") else value
|
|
1205
|
+
_PROOF_CACHE[(image, label)] = (now, value)
|
|
1206
|
+
return value
|
|
1128
1207
|
|
|
1129
1208
|
|
|
1130
1209
|
def sandbox_refusal(cwd: str) -> Optional[str]:
|
|
@@ -1163,7 +1242,11 @@ def sandbox_argv(podman: str, sandbox: dict, cwd: str, argv: Union[str, List[str
|
|
|
1163
1242
|
passed += ["-e", f"{key}={host_env[key]}"]
|
|
1164
1243
|
for key, value in (variables or {}).items():
|
|
1165
1244
|
passed += ["-e", f"{key}={value}"]
|
|
1166
|
-
|
|
1245
|
+
# --init: podman's init (catatonit) is the container's PID 1 and reaps what the command
|
|
1246
|
+
# started and killed. Without one a killed child stays a zombie nobody waits for, and a
|
|
1247
|
+
# receipt that checks its children are gone reads it as alive (the image proof on the
|
|
1248
|
+
# driver, 2026-09-30: two tests failed that way)
|
|
1249
|
+
return [podman, "run", "--rm", "--pull=never", "--init", "--name", name,
|
|
1167
1250
|
"--network=none", "--userns=keep-id", "--cap-drop=all",
|
|
1168
1251
|
"--security-opt=no-new-privileges", "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev",
|
|
1169
1252
|
"--pids-limit", str(sandbox["pids"]), "--memory", str(sandbox["memory"]),
|
|
@@ -1199,9 +1282,9 @@ def network_refusal(text: str) -> Optional[str]:
|
|
|
1199
1282
|
return None
|
|
1200
1283
|
|
|
1201
1284
|
|
|
1202
|
-
# what a command says when the image does not hold what it imports or runs:
|
|
1203
|
-
#
|
|
1204
|
-
#
|
|
1285
|
+
# what a command says when the image does not hold what it imports or runs: an image not
|
|
1286
|
+
# proven for the worktree's lockfile may lack anything, and even a proven one holds only
|
|
1287
|
+
# what the lockfile names, so a missing module is the image's gap, not the receipt's
|
|
1205
1288
|
IMAGE_LACKS = "sandboxed, the image lacks"
|
|
1206
1289
|
_MISSING = re.compile(r"No module named '?([A-Za-z0-9_.]+)'?|(?:^|: )([A-Za-z0-9_.+-]+): (?:command )?not found")
|
|
1207
1290
|
|
|
@@ -1215,11 +1298,15 @@ def image_lacks(text: str) -> Optional[str]:
|
|
|
1215
1298
|
return None
|
|
1216
1299
|
|
|
1217
1300
|
|
|
1218
|
-
def sandbox_failure(image: str, fresh: dict, expected_exit, cmd
|
|
1301
|
+
def sandbox_failure(image: str, fresh: dict, expected_exit, cmd,
|
|
1302
|
+
unproven: Optional[str] = None) -> Optional[str]:
|
|
1219
1303
|
"""Why a sandboxed run that disagreed with its expectation is no answer, or None when it
|
|
1220
1304
|
is one: podman's own failure (exit 125: no image by that name), a program the image
|
|
1221
|
-
cannot run (126, 127), a network the sandbox does not have,
|
|
1222
|
-
|
|
1305
|
+
cannot run (126, 127), a network the sandbox does not have, a module the image does not
|
|
1306
|
+
hold -- and any failure at all in an image not proven complete for the worktree's
|
|
1307
|
+
lockfile (``unproven``, image_proof's reason), since that failure may be the image's.
|
|
1308
|
+
Only a proven image's failure refutes. The re-check and the acceptance check read a
|
|
1309
|
+
sandboxed run alike.
|
|
1223
1310
|
|
|
1224
1311
|
The last two are read from the run's own output, which its author wrote: a failing
|
|
1225
1312
|
command can print its way to "sandboxed, no network". That buys it no verification --
|
|
@@ -1237,6 +1324,8 @@ def sandbox_failure(image: str, fresh: dict, expected_exit, cmd) -> Optional[str
|
|
|
1237
1324
|
lacking = image_lacks(tail)
|
|
1238
1325
|
if lacking:
|
|
1239
1326
|
return f"{IMAGE_LACKS} {lacking}"
|
|
1327
|
+
if unproven:
|
|
1328
|
+
return f"the re-run failed in {image}, which is not proven for this worktree's lockfile ({unproven}); not a refutation"
|
|
1240
1329
|
return None
|
|
1241
1330
|
|
|
1242
1331
|
|
|
@@ -1441,6 +1530,12 @@ def _git(args: Sequence[str], cwd: str) -> subprocess.CompletedProcess:
|
|
|
1441
1530
|
timeout=GIT_CHECK_SECONDS)
|
|
1442
1531
|
|
|
1443
1532
|
|
|
1533
|
+
class BindingError(ValueError):
|
|
1534
|
+
"""A binding that could not be built on this host: a commit its repository does not
|
|
1535
|
+
hold, a file that is missing or not a regular one. The message is this host's detail
|
|
1536
|
+
(its paths, git's stderr); a server answers a remote caller without it (serve.py)."""
|
|
1537
|
+
|
|
1538
|
+
|
|
1444
1539
|
def commit_binding(repo: Optional[str] = None, sha: Optional[str] = None, branch: Optional[str] = None) -> dict:
|
|
1445
1540
|
"""Bind to a commit. Raises ValueError when the sha does not resolve in the repo:
|
|
1446
1541
|
a binding to a commit that does not exist is not evidence of anything."""
|
|
@@ -1996,7 +2091,11 @@ def check(body: dict, cwd: Optional[str] = None, strict: bool = False, timeout:
|
|
|
1996
2091
|
if notes.get("sandboxed"):
|
|
1997
2092
|
# a disagreement the sandbox itself caused is a question it could not ask there,
|
|
1998
2093
|
# not a different answer (the PE's ruling on ctr_543e, for the network)
|
|
1999
|
-
|
|
2094
|
+
# the image's proof is asked only of a disagreement: a match verifies in any image
|
|
2095
|
+
unproven = (image_proof(notes["sandbox"], notes["sandbox_cwd"])
|
|
2096
|
+
if outcome == "mismatch" and notes.get("sandbox") else None)
|
|
2097
|
+
reason = (sandbox_failure(notes["sandboxed"], notes.get("fresh") or {}, body.get("exit"), body.get("cmd"),
|
|
2098
|
+
unproven=unproven)
|
|
2000
2099
|
if outcome == "mismatch" else None)
|
|
2001
2100
|
if reason:
|
|
2002
2101
|
outcome = "error"
|
|
@@ -2181,6 +2280,7 @@ def _check_binding(body: dict, cwd: Optional[str] = None, strict: bool = False,
|
|
|
2181
2280
|
if sandbox is not None: # check() reads a disagreement through sandbox_failure
|
|
2182
2281
|
notes["sandboxed"] = sandbox["image"]
|
|
2183
2282
|
notes["fresh"] = fresh
|
|
2283
|
+
notes["sandbox"], notes["sandbox_cwd"] = sandbox, run_cwd
|
|
2184
2284
|
if fresh.get("start_error") and fresh["exit"] != body.get("exit"):
|
|
2185
2285
|
# The command never started here and the recording does not say it should
|
|
2186
2286
|
# not have, so nothing was asked and nothing disagreed: a `chmod`, a
|
|
@@ -130,6 +130,7 @@ UNTRUSTED_POLICY = {
|
|
|
130
130
|
"recheck_allow": list(STARTER_CONFIG["recheck_allow"]), # not the clone's idea of "safe"
|
|
131
131
|
"recheck_allow_shell": False,
|
|
132
132
|
"recheck_sandbox": None, # which image a re-check runs in, and whether: the machine's, never a clone's
|
|
133
|
+
"retrieval": None, # the plugin a compile runs (ctr_9e166c2f291b829b): the machine's, never a clone's
|
|
133
134
|
"redact": [], # an attacker-supplied regex can hang capture (catastrophic backtracking)
|
|
134
135
|
"path_map": [], # where a re-check runs
|
|
135
136
|
"org_path": None, # what it reads and writes outside the checkout
|
|
@@ -30,7 +30,7 @@ import sys
|
|
|
30
30
|
import traceback
|
|
31
31
|
from datetime import timedelta
|
|
32
32
|
from pathlib import Path
|
|
33
|
-
from typing import Any, Dict, Optional
|
|
33
|
+
from typing import Any, Dict, List, Optional
|
|
34
34
|
|
|
35
35
|
from . import __version__
|
|
36
36
|
from . import approvals as approvals_mod
|
|
@@ -706,12 +706,7 @@ class Server:
|
|
|
706
706
|
if any(not isinstance(item, str) for item in evidence):
|
|
707
707
|
# an inline evidence object is an agent typing its own receipt
|
|
708
708
|
return self._text("rejected: evidence must be receipt ids; use memory_run", is_error=True)
|
|
709
|
-
|
|
710
|
-
evidence.append(ev.commit_binding(str(ledger.root), args["commit"]))
|
|
711
|
-
for f in args.get("files") or []:
|
|
712
|
-
evidence.append(ev.file_binding(f))
|
|
713
|
-
for f in args.get("env_files") or []:
|
|
714
|
-
evidence.append(ev.file_binding(f, role="environment"))
|
|
709
|
+
evidence += self._bindings(args, ledger)
|
|
715
710
|
if evidence:
|
|
716
711
|
body["evidence"] = evidence
|
|
717
712
|
for key in ("rationale", "example", "outcome", "source", "contract", "confidence", "expect"):
|
|
@@ -736,6 +731,20 @@ class Server:
|
|
|
736
731
|
out["warning"] = warning
|
|
737
732
|
return self._text(json.dumps(out))
|
|
738
733
|
|
|
734
|
+
def _bindings(self, args: dict, ledger) -> List[dict]:
|
|
735
|
+
"""The bindings a claim's arguments ask for, built on this host: the commit in this
|
|
736
|
+
ledger's repository, then the files by path. What building one raises names this
|
|
737
|
+
host (its repository, git's stderr, which paths exist), which the caller of a local
|
|
738
|
+
server already knows; the served tool keeps it from a remote one (serve.py)."""
|
|
739
|
+
built: List[dict] = []
|
|
740
|
+
if args.get("commit"):
|
|
741
|
+
built.append(ev.commit_binding(str(ledger.root), args["commit"]))
|
|
742
|
+
for f in args.get("files") or []:
|
|
743
|
+
built.append(ev.file_binding(f))
|
|
744
|
+
for f in args.get("env_files") or []:
|
|
745
|
+
built.append(ev.file_binding(f, role="environment"))
|
|
746
|
+
return built
|
|
747
|
+
|
|
739
748
|
def tool_memory_run(self, args: dict) -> dict:
|
|
740
749
|
if not self.allow_run:
|
|
741
750
|
return self._text(
|
|
@@ -48,6 +48,21 @@ RECHECK_TIMEOUT = 660.0
|
|
|
48
48
|
# the client speaks the revision the stdio server speaks; the server accepts three
|
|
49
49
|
MCP_VERSION = "2025-06-18"
|
|
50
50
|
|
|
51
|
+
# How much of a reply this client will hold (ctr_cfbd768152e255a4). A hostile, broken or
|
|
52
|
+
# intercepted replica could otherwise send a body with no end, and the CLI, a hook or the
|
|
53
|
+
# relay would read all of it into memory. The largest legitimate reply is one record, and
|
|
54
|
+
# serve takes a posted record of up to serve.MAX_BODY_BYTES (8 MiB). On 2026-09-30 the
|
|
55
|
+
# fleet's 3,171 records had a largest of 51 KB, the 500 largest together (a worst page at
|
|
56
|
+
# PAGE_MAX) were 1.8 MiB, /audit was 262 KB and the whole ledger 4 MiB. So 32 MiB is four
|
|
57
|
+
# times the largest record the server accepts and seventeen times today's worst page, and
|
|
58
|
+
# it is all a replica can make the client hold. A test pins these against serve's.
|
|
59
|
+
REPLY_MAX_BYTES = 32 * 1024 * 1024
|
|
60
|
+
# the event stream never ends, so it is capped per line: one data line carries one record
|
|
61
|
+
STREAM_LINE_MAX_BYTES = 16 * 1024 * 1024
|
|
62
|
+
# an error body is {"error": "..."} and only its first 500 characters are ever shown, so it
|
|
63
|
+
# is read this far and no further; a longer one is cut, not refused, as it is already an error
|
|
64
|
+
ERROR_BODY_MAX_BYTES = 64 * 1024
|
|
65
|
+
|
|
51
66
|
LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1", "[::1]"}
|
|
52
67
|
|
|
53
68
|
|
|
@@ -300,11 +315,11 @@ class Client:
|
|
|
300
315
|
req.add_header(key, value)
|
|
301
316
|
try:
|
|
302
317
|
with self.opener.open(req, timeout=timeout or self.timeout) as resp:
|
|
303
|
-
return resp.status, dict(resp.headers), resp
|
|
318
|
+
return resp.status, dict(resp.headers), _capped_body(resp, path).decode("utf-8", "replace")
|
|
304
319
|
except urllib.error.HTTPError as exc:
|
|
305
320
|
if 300 <= exc.code < 400:
|
|
306
321
|
raise RemoteError(exc.code, _redirect_refusal(exc.headers.get("Location"))) from exc
|
|
307
|
-
raise RemoteError(exc.code, _message(exc.code, exc.read())) from exc
|
|
322
|
+
raise RemoteError(exc.code, _message(exc.code, exc.read(ERROR_BODY_MAX_BYTES))) from exc
|
|
308
323
|
except urllib.error.URLError as exc:
|
|
309
324
|
raise RemoteError(0, f"cannot reach {self.base}: {exc.reason}") from exc
|
|
310
325
|
except (TimeoutError, OSError) as exc: # a socket timeout on a read already under way
|
|
@@ -428,7 +443,8 @@ class Client:
|
|
|
428
443
|
nothing on the wire: every way a stream can stop has the same answer, which is to
|
|
429
444
|
reconnect with the last id that did arrive. RemoteError is for the answer that
|
|
430
445
|
never became a stream at all, which is a different thing and the caller's to
|
|
431
|
-
report
|
|
446
|
+
report, and for a line longer than STREAM_LINE_MAX_BYTES, which no record makes
|
|
447
|
+
and which is read no further. This method does no work until it is iterated. ``alive``, when given, is
|
|
432
448
|
called for every line the server sends, a heartbeat included: a caller that backs off
|
|
433
449
|
between connections can tell a live stream that was quiet from one that sent nothing.
|
|
434
450
|
``on_open``, when given, is handed a ``close()`` once the stream is open, which any
|
|
@@ -452,7 +468,7 @@ class Client:
|
|
|
452
468
|
except urllib.error.HTTPError as exc:
|
|
453
469
|
if 300 <= exc.code < 400:
|
|
454
470
|
raise RemoteError(exc.code, _redirect_refusal(exc.headers.get("Location"))) from exc
|
|
455
|
-
raise RemoteError(exc.code, _message(exc.code, exc.read())) from exc
|
|
471
|
+
raise RemoteError(exc.code, _message(exc.code, exc.read(ERROR_BODY_MAX_BYTES))) from exc
|
|
456
472
|
except urllib.error.URLError as exc:
|
|
457
473
|
raise RemoteError(0, f"cannot reach {self.base}: {exc.reason}") from exc
|
|
458
474
|
except (TimeoutError, OSError) as exc:
|
|
@@ -465,11 +481,14 @@ class Client:
|
|
|
465
481
|
try:
|
|
466
482
|
while True:
|
|
467
483
|
try:
|
|
468
|
-
raw = stream.readline()
|
|
484
|
+
raw = stream.readline(STREAM_LINE_MAX_BYTES + 1)
|
|
469
485
|
except (TimeoutError, OSError): # a socket timeout is the idle stream's end
|
|
470
486
|
return
|
|
471
487
|
if not raw:
|
|
472
488
|
return
|
|
489
|
+
if len(raw) > STREAM_LINE_MAX_BYTES: # a line with no end in sight: stop reading
|
|
490
|
+
raise RemoteError(stream.status, f"/events sent a line of more than {STREAM_LINE_MAX_BYTES} "
|
|
491
|
+
f"bytes (STREAM_LINE_MAX_BYTES); stopped reading the stream")
|
|
473
492
|
if alive is not None:
|
|
474
493
|
alive()
|
|
475
494
|
line = raw.decode("utf-8", "replace").rstrip("\n").rstrip("\r")
|
|
@@ -575,6 +594,25 @@ def _redirect_refusal(location: Optional[str]) -> str:
|
|
|
575
594
|
"the host it names. Point --remote at the final URL.")
|
|
576
595
|
|
|
577
596
|
|
|
597
|
+
def _capped_body(resp, path: str) -> bytes:
|
|
598
|
+
"""A reply's body, or RemoteError once it is over REPLY_MAX_BYTES.
|
|
599
|
+
|
|
600
|
+
A declared Content-Length over the cap is refused before any of it is read, and a body
|
|
601
|
+
that declares none, or declares less than it sends, is read one byte past the cap and
|
|
602
|
+
no further. The message names the cap and the path; never the query, which carries
|
|
603
|
+
what was asked, and never the token.
|
|
604
|
+
"""
|
|
605
|
+
where = path.split("?", 1)[0]
|
|
606
|
+
refusal = f"{where} answered with more than {REPLY_MAX_BYTES} bytes (REPLY_MAX_BYTES); stopped reading"
|
|
607
|
+
declared = resp.headers.get("Content-Length")
|
|
608
|
+
if declared is not None and declared.strip().isdigit() and int(declared) > REPLY_MAX_BYTES:
|
|
609
|
+
raise RemoteError(resp.status, refusal)
|
|
610
|
+
body = resp.read(REPLY_MAX_BYTES + 1)
|
|
611
|
+
if len(body) > REPLY_MAX_BYTES:
|
|
612
|
+
raise RemoteError(resp.status, refusal)
|
|
613
|
+
return body
|
|
614
|
+
|
|
615
|
+
|
|
578
616
|
def _message(status: int, payload: bytes) -> str:
|
|
579
617
|
"""The server's own message for a failed request: every error body in this API is
|
|
580
618
|
``{"error": "..."}``, and anything else (a proxy's HTML) is shown as it arrived."""
|
|
@@ -23,6 +23,7 @@ SECTION_TITLES = {
|
|
|
23
23
|
"decisions": "Decisions",
|
|
24
24
|
"directions": "Directions (what to try next, what to avoid)",
|
|
25
25
|
"working": "Working memory (evidenced or asserted; confirm before relying on)",
|
|
26
|
+
"retrieved": "Retrieved (what the retrieval plugin found for this task; confirm before relying on)",
|
|
26
27
|
"exemplars": "Exemplars",
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -161,7 +162,8 @@ def _markdown_lines(c: Compiled, h: str) -> Tuple[List[str], int]:
|
|
|
161
162
|
that stays the same bytes while only working memory changes (roadmap 8.5.1)."""
|
|
162
163
|
lines: List[str] = _header_lines(c, h)
|
|
163
164
|
prefix = None
|
|
164
|
-
for name in ("constraints", "contract", "cross_check", "do_not_retry", "verified", "decisions", "directions", "working",
|
|
165
|
+
for name in ("constraints", "contract", "cross_check", "do_not_retry", "verified", "decisions", "directions", "working",
|
|
166
|
+
"retrieved", "exemplars"):
|
|
165
167
|
if name == "working":
|
|
166
168
|
prefix = len(lines)
|
|
167
169
|
if name == "contract":
|
|
@@ -313,6 +315,10 @@ def render_json(c: Compiled) -> str:
|
|
|
313
315
|
"obligations": c.obligations,
|
|
314
316
|
"excluded": [{"id": i, "reason": r} for i, r in c.excluded],
|
|
315
317
|
}
|
|
318
|
+
if c.retrieval is not None:
|
|
319
|
+
# only when a plugin took part, as the receipt carries it: without the key the json
|
|
320
|
+
# view is what it was (the review of #149)
|
|
321
|
+
payload["retrieval"] = c.retrieval
|
|
316
322
|
return json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
|
317
323
|
|
|
318
324
|
|