superlocalmemory 3.8.13 → 4.0.0
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.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
|
|
12
|
+
MANIFEST_V1: int = 1
|
|
13
|
+
MANIFEST_V2: int = 2
|
|
14
|
+
|
|
15
|
+
from superlocalmemory.core.transactions.obligations import Obligation
|
|
16
|
+
from superlocalmemory.core.transactions.owners import (
|
|
17
|
+
ObligationKind,
|
|
18
|
+
ObligationState,
|
|
19
|
+
is_terminal_success,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ManifestState(StrEnum):
|
|
24
|
+
COMPLETE = "COMPLETE"
|
|
25
|
+
DEGRADED = "DEGRADED"
|
|
26
|
+
FAILED = "FAILED"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class OwnerEvidence:
|
|
31
|
+
owner: str
|
|
32
|
+
kind: ObligationKind
|
|
33
|
+
state: ObligationState
|
|
34
|
+
revision: int
|
|
35
|
+
checksum: str
|
|
36
|
+
|
|
37
|
+
def as_dict(self) -> dict[str, object]:
|
|
38
|
+
return {
|
|
39
|
+
"owner": self.owner,
|
|
40
|
+
"kind": str(self.kind),
|
|
41
|
+
"state": str(self.state),
|
|
42
|
+
"revision": self.revision,
|
|
43
|
+
"checksum": self.checksum,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class CompletionManifest:
|
|
49
|
+
operation_id: str
|
|
50
|
+
profile_id: str
|
|
51
|
+
state: ManifestState
|
|
52
|
+
all_met: bool
|
|
53
|
+
obligation_count: int
|
|
54
|
+
owner_evidence: tuple[OwnerEvidence, ...]
|
|
55
|
+
manifest_hash: str
|
|
56
|
+
created_at: float
|
|
57
|
+
updated_at: float
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_evidence(obligations: Iterable[Obligation]) -> tuple[OwnerEvidence, ...]:
|
|
61
|
+
evidence = [
|
|
62
|
+
OwnerEvidence(
|
|
63
|
+
owner=o.owner,
|
|
64
|
+
kind=o.kind,
|
|
65
|
+
state=o.state,
|
|
66
|
+
revision=o.revision,
|
|
67
|
+
checksum=o.checksum or "",
|
|
68
|
+
)
|
|
69
|
+
for o in obligations
|
|
70
|
+
]
|
|
71
|
+
evidence.sort(key=lambda e: (e.owner, str(e.kind), e.revision))
|
|
72
|
+
return tuple(evidence)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _sorted_evidence_dicts(evidence: Iterable[OwnerEvidence]) -> list[dict[str, object]]:
|
|
76
|
+
return sorted(
|
|
77
|
+
(e.as_dict() for e in evidence),
|
|
78
|
+
key=lambda d: (d["owner"], d["kind"], d["revision"]),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def evidence_json(evidence: Iterable[OwnerEvidence]) -> str:
|
|
83
|
+
return json.dumps(
|
|
84
|
+
_sorted_evidence_dicts(evidence),
|
|
85
|
+
sort_keys=True,
|
|
86
|
+
separators=(",", ":"),
|
|
87
|
+
ensure_ascii=False,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def compute_envelope_hash(
|
|
92
|
+
*,
|
|
93
|
+
operation_id: str,
|
|
94
|
+
profile_id: str,
|
|
95
|
+
state: ManifestState,
|
|
96
|
+
all_met: bool,
|
|
97
|
+
obligation_count: int,
|
|
98
|
+
evidence: Iterable[OwnerEvidence],
|
|
99
|
+
) -> str:
|
|
100
|
+
envelope = {
|
|
101
|
+
"operation_id": operation_id,
|
|
102
|
+
"profile_id": profile_id,
|
|
103
|
+
"state": str(state),
|
|
104
|
+
"all_met": bool(all_met),
|
|
105
|
+
"obligation_count": obligation_count,
|
|
106
|
+
"evidence": _sorted_evidence_dicts(evidence),
|
|
107
|
+
}
|
|
108
|
+
canonical = json.dumps(
|
|
109
|
+
envelope, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
110
|
+
)
|
|
111
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def hash_envelope_fields(
|
|
115
|
+
*,
|
|
116
|
+
operation_id: str,
|
|
117
|
+
profile_id: str,
|
|
118
|
+
state: str,
|
|
119
|
+
all_met: bool,
|
|
120
|
+
obligation_count: int,
|
|
121
|
+
evidence_dicts: list[dict[str, object]],
|
|
122
|
+
) -> str:
|
|
123
|
+
envelope = {
|
|
124
|
+
"operation_id": operation_id,
|
|
125
|
+
"profile_id": profile_id,
|
|
126
|
+
"state": state,
|
|
127
|
+
"all_met": bool(all_met),
|
|
128
|
+
"obligation_count": obligation_count,
|
|
129
|
+
"evidence": evidence_dicts,
|
|
130
|
+
}
|
|
131
|
+
canonical = json.dumps(
|
|
132
|
+
envelope, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
133
|
+
)
|
|
134
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _canonical_envelope(
|
|
138
|
+
*,
|
|
139
|
+
operation_id: str,
|
|
140
|
+
profile_id: str,
|
|
141
|
+
state: str,
|
|
142
|
+
all_met: bool,
|
|
143
|
+
obligation_count: int,
|
|
144
|
+
evidence_dicts: list[dict[str, object]],
|
|
145
|
+
manifest_version: int | None = None,
|
|
146
|
+
) -> bytes:
|
|
147
|
+
"""Produce the deterministic byte representation of the manifest envelope.
|
|
148
|
+
|
|
149
|
+
When ``manifest_version`` is provided (v2+), it is bound into the canonical
|
|
150
|
+
bytes so a downgrade from v2 → v1 is detected by the MAC mismatch: the v1
|
|
151
|
+
SHA path does NOT include version, so any v2 HMAC covers a strictly different
|
|
152
|
+
byte string than any v1 SHA.
|
|
153
|
+
"""
|
|
154
|
+
envelope: dict[str, object] = {
|
|
155
|
+
"operation_id": operation_id,
|
|
156
|
+
"profile_id": profile_id,
|
|
157
|
+
"state": state,
|
|
158
|
+
"all_met": bool(all_met),
|
|
159
|
+
"obligation_count": obligation_count,
|
|
160
|
+
"evidence": evidence_dicts,
|
|
161
|
+
}
|
|
162
|
+
if manifest_version is not None:
|
|
163
|
+
envelope["manifest_version"] = manifest_version
|
|
164
|
+
return json.dumps(
|
|
165
|
+
envelope, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
|
|
166
|
+
).encode("utf-8")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def compute_envelope_hmac(
|
|
170
|
+
*,
|
|
171
|
+
operation_id: str,
|
|
172
|
+
profile_id: str,
|
|
173
|
+
state: ManifestState,
|
|
174
|
+
all_met: bool,
|
|
175
|
+
obligation_count: int,
|
|
176
|
+
evidence: Iterable[OwnerEvidence],
|
|
177
|
+
key: bytes | None = None,
|
|
178
|
+
) -> str:
|
|
179
|
+
"""HMAC-SHA256 keyed manifest hash (v2). ``key`` is injected in tests;
|
|
180
|
+
production code passes ``key=None`` to auto-derive from the installation key."""
|
|
181
|
+
from superlocalmemory.core.transactions.manifest_key import (
|
|
182
|
+
compute_hmac,
|
|
183
|
+
derive_manifest_hmac_key,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
actual_key = key if key is not None else derive_manifest_hmac_key()
|
|
187
|
+
payload = _canonical_envelope(
|
|
188
|
+
operation_id=operation_id,
|
|
189
|
+
profile_id=profile_id,
|
|
190
|
+
state=str(state),
|
|
191
|
+
all_met=all_met,
|
|
192
|
+
obligation_count=obligation_count,
|
|
193
|
+
evidence_dicts=_sorted_evidence_dicts(evidence),
|
|
194
|
+
manifest_version=MANIFEST_V2,
|
|
195
|
+
)
|
|
196
|
+
return compute_hmac(actual_key, payload)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def verify_envelope_hmac(
|
|
200
|
+
*,
|
|
201
|
+
operation_id: str,
|
|
202
|
+
profile_id: str,
|
|
203
|
+
state: str,
|
|
204
|
+
all_met: bool,
|
|
205
|
+
obligation_count: int,
|
|
206
|
+
evidence_dicts: list[dict[str, object]],
|
|
207
|
+
stored_mac: str,
|
|
208
|
+
key: bytes | None = None,
|
|
209
|
+
) -> bool:
|
|
210
|
+
"""Constant-time verify an HMAC-keyed manifest hash (v2)."""
|
|
211
|
+
from superlocalmemory.core.transactions.manifest_key import (
|
|
212
|
+
derive_manifest_hmac_key,
|
|
213
|
+
verify_hmac,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
actual_key = key if key is not None else derive_manifest_hmac_key()
|
|
217
|
+
payload = _canonical_envelope(
|
|
218
|
+
operation_id=operation_id,
|
|
219
|
+
profile_id=profile_id,
|
|
220
|
+
state=state,
|
|
221
|
+
all_met=all_met,
|
|
222
|
+
obligation_count=obligation_count,
|
|
223
|
+
evidence_dicts=evidence_dicts,
|
|
224
|
+
manifest_version=MANIFEST_V2,
|
|
225
|
+
)
|
|
226
|
+
return verify_hmac(stored_mac, actual_key, payload)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def derive_state(
|
|
230
|
+
obligations: tuple[Obligation, ...], *, canonical_committed: bool,
|
|
231
|
+
) -> tuple[ManifestState, bool]:
|
|
232
|
+
if not canonical_committed:
|
|
233
|
+
return ManifestState.FAILED, False
|
|
234
|
+
if not obligations:
|
|
235
|
+
return ManifestState.FAILED, False
|
|
236
|
+
all_met = all(is_terminal_success(o.state) for o in obligations)
|
|
237
|
+
if all_met:
|
|
238
|
+
return ManifestState.COMPLETE, True
|
|
239
|
+
return ManifestState.DEGRADED, False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
__all__ = [
|
|
243
|
+
"MANIFEST_V1",
|
|
244
|
+
"MANIFEST_V2",
|
|
245
|
+
"CompletionManifest",
|
|
246
|
+
"ManifestState",
|
|
247
|
+
"OwnerEvidence",
|
|
248
|
+
"build_evidence",
|
|
249
|
+
"compute_envelope_hash",
|
|
250
|
+
"compute_envelope_hmac",
|
|
251
|
+
"derive_state",
|
|
252
|
+
"evidence_json",
|
|
253
|
+
"hash_envelope_fields",
|
|
254
|
+
"verify_envelope_hmac",
|
|
255
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Installation-bound HMAC signing key for completion manifests and erasure receipts.
|
|
5
|
+
|
|
6
|
+
The signing key is a 32-byte secret persisted at
|
|
7
|
+
``~/.superlocalmemory/.manifest_signing_key`` (mode 0600). It is generated
|
|
8
|
+
once on first use and read on every subsequent call. Purpose-specific
|
|
9
|
+
sub-keys are derived via HMAC so a key compromise in one context cannot be
|
|
10
|
+
used to forge a receipt in another.
|
|
11
|
+
|
|
12
|
+
The key is NEVER logged or included in any error message.
|
|
13
|
+
|
|
14
|
+
Key file location: resolved via ``state_path(".manifest_signing_key")``.
|
|
15
|
+
In tests, monkeypatch ``_signing_key_path`` and/or ``_ensure_signing_key``
|
|
16
|
+
to return a deterministic path or byte sequence.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import hmac as _hmac
|
|
23
|
+
import os
|
|
24
|
+
import secrets
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
_KEY_CONTEXT_MANIFEST: bytes = b"slm-manifest-v2"
|
|
29
|
+
_KEY_CONTEXT_RECEIPT: bytes = b"slm-receipt-v2"
|
|
30
|
+
_MANIFEST_VERSION: int = 2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _signing_key_path() -> Path:
|
|
34
|
+
from superlocalmemory.infra.data_root import state_path
|
|
35
|
+
|
|
36
|
+
return state_path(".manifest_signing_key")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_signing_key() -> bytes:
|
|
40
|
+
"""Read the 32-byte hex signing key, creating it on first call.
|
|
41
|
+
|
|
42
|
+
Returns the raw bytes of the key (NOT the hex string).
|
|
43
|
+
|
|
44
|
+
Raises RuntimeError if the key cannot be obtained (corrupt, unreadable,
|
|
45
|
+
permission error). Ephemeral keys are NEVER returned — they would
|
|
46
|
+
permanently desync multi-process daemons and silently break all
|
|
47
|
+
verification of previously-signed manifests.
|
|
48
|
+
"""
|
|
49
|
+
key_path = _signing_key_path()
|
|
50
|
+
try:
|
|
51
|
+
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"signing key: cannot create key directory (key never logged)"
|
|
55
|
+
) from exc
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
key_exists = key_path.exists()
|
|
59
|
+
except OSError as exc:
|
|
60
|
+
# stat() itself can fail (e.g. an unreadable parent dir). Fail closed
|
|
61
|
+
# with the same RuntimeError contract — never leak a raw OSError or
|
|
62
|
+
# fall through to an ephemeral key.
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
"signing key: cannot access key path (key never logged)"
|
|
65
|
+
) from exc
|
|
66
|
+
|
|
67
|
+
if key_exists:
|
|
68
|
+
try:
|
|
69
|
+
raw = key_path.read_text(encoding="utf-8").strip()
|
|
70
|
+
except OSError as exc:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"signing key: cannot read existing key file (key never logged)"
|
|
73
|
+
) from exc
|
|
74
|
+
if len(raw) != 64:
|
|
75
|
+
raise RuntimeError(
|
|
76
|
+
"signing key: file has unexpected length (corrupt or truncated) — "
|
|
77
|
+
"remove the file and restart to generate a fresh key"
|
|
78
|
+
)
|
|
79
|
+
return bytes.fromhex(raw)
|
|
80
|
+
|
|
81
|
+
# First-use: generate and persist atomically.
|
|
82
|
+
raw = secrets.token_hex(32)
|
|
83
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
84
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
85
|
+
flags |= os.O_NOFOLLOW
|
|
86
|
+
try:
|
|
87
|
+
fd = os.open(str(key_path), flags, 0o600)
|
|
88
|
+
try:
|
|
89
|
+
os.write(fd, raw.encode("utf-8"))
|
|
90
|
+
finally:
|
|
91
|
+
os.close(fd)
|
|
92
|
+
except FileExistsError:
|
|
93
|
+
# Another process won the race — read its key (never return our own).
|
|
94
|
+
try:
|
|
95
|
+
existing = key_path.read_text(encoding="utf-8").strip()
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
raise RuntimeError(
|
|
98
|
+
"signing key: race winner's key unreadable (key never logged)"
|
|
99
|
+
) from exc
|
|
100
|
+
if len(existing) != 64:
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
"signing key: race winner wrote a corrupt key (key never logged)"
|
|
103
|
+
)
|
|
104
|
+
return bytes.fromhex(existing)
|
|
105
|
+
except OSError as exc:
|
|
106
|
+
# Permission / symlink error — never return an ephemeral key.
|
|
107
|
+
raise RuntimeError(
|
|
108
|
+
"signing key: cannot create key file (key never logged)"
|
|
109
|
+
) from exc
|
|
110
|
+
|
|
111
|
+
if sys.platform != "win32":
|
|
112
|
+
try:
|
|
113
|
+
os.chmod(str(key_path), 0o600)
|
|
114
|
+
except OSError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
return bytes.fromhex(raw)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def derive_manifest_hmac_key() -> bytes:
|
|
121
|
+
"""Return the HMAC sub-key for completion manifests."""
|
|
122
|
+
master = _ensure_signing_key()
|
|
123
|
+
return _hmac.new(master, _KEY_CONTEXT_MANIFEST, hashlib.sha256).digest()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def derive_receipt_hmac_key() -> bytes:
|
|
127
|
+
"""Return the HMAC sub-key for erasure receipts."""
|
|
128
|
+
master = _ensure_signing_key()
|
|
129
|
+
return _hmac.new(master, _KEY_CONTEXT_RECEIPT, hashlib.sha256).digest()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def compute_hmac(key: bytes, payload: bytes) -> str:
|
|
133
|
+
"""Return HMAC-SHA256 of ``payload`` under ``key`` as a hex string."""
|
|
134
|
+
return _hmac.new(key, payload, hashlib.sha256).hexdigest()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def verify_hmac(stored_mac: str, key: bytes, payload: bytes) -> bool:
|
|
138
|
+
"""Constant-time verify that ``stored_mac`` matches HMAC of ``payload`` under ``key``."""
|
|
139
|
+
try:
|
|
140
|
+
expected = _hmac.new(key, payload, hashlib.sha256).digest()
|
|
141
|
+
stored_bytes = bytes.fromhex(stored_mac)
|
|
142
|
+
except (ValueError, TypeError):
|
|
143
|
+
return False
|
|
144
|
+
return _hmac.compare_digest(expected, stored_bytes)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = [
|
|
148
|
+
"MANIFEST_VERSION",
|
|
149
|
+
"compute_hmac",
|
|
150
|
+
"derive_manifest_hmac_key",
|
|
151
|
+
"derive_receipt_hmac_key",
|
|
152
|
+
"verify_hmac",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
MANIFEST_VERSION = _MANIFEST_VERSION
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import sqlite3
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Iterable, Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from superlocalmemory.core.transactions.owners import (
|
|
15
|
+
ObligationKind,
|
|
16
|
+
ObligationState,
|
|
17
|
+
OperationContext,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ObligationConflictError(ValueError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class Obligation:
|
|
27
|
+
operation_id: str
|
|
28
|
+
profile_id: str
|
|
29
|
+
owner: str
|
|
30
|
+
kind: ObligationKind
|
|
31
|
+
subject_id: str
|
|
32
|
+
revision: int
|
|
33
|
+
state: ObligationState
|
|
34
|
+
checksum: str | None
|
|
35
|
+
detail: Mapping[str, Any] | None
|
|
36
|
+
attempts: int
|
|
37
|
+
context_digest: str | None
|
|
38
|
+
verify_attempts: int
|
|
39
|
+
last_verified_at: float | None
|
|
40
|
+
created_at: float
|
|
41
|
+
updated_at: float
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_SELECT_COLUMNS = (
|
|
45
|
+
"operation_id, profile_id, owner, kind, subject_id, revision, state, "
|
|
46
|
+
"checksum, detail, attempts, context_digest, verify_attempts, "
|
|
47
|
+
"last_verified_at, created_at, updated_at"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def context_digest(
|
|
52
|
+
context: OperationContext, owner: str, kind: ObligationKind,
|
|
53
|
+
) -> str:
|
|
54
|
+
payload = "\0".join([
|
|
55
|
+
context.operation_id,
|
|
56
|
+
context.profile_id,
|
|
57
|
+
owner,
|
|
58
|
+
str(kind),
|
|
59
|
+
])
|
|
60
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ObligationLedger:
|
|
64
|
+
def record(
|
|
65
|
+
self,
|
|
66
|
+
conn: sqlite3.Connection,
|
|
67
|
+
context: OperationContext,
|
|
68
|
+
owner: str,
|
|
69
|
+
kind: ObligationKind,
|
|
70
|
+
) -> None:
|
|
71
|
+
digest = context_digest(context, owner, kind)
|
|
72
|
+
existing = conn.execute(
|
|
73
|
+
"SELECT context_digest, profile_id FROM projection_obligations "
|
|
74
|
+
"WHERE operation_id = ? AND owner = ? AND kind = ?",
|
|
75
|
+
(context.operation_id, owner, str(kind)),
|
|
76
|
+
).fetchone()
|
|
77
|
+
if existing is not None:
|
|
78
|
+
stored, stored_profile = existing[0], existing[1]
|
|
79
|
+
if stored_profile != context.profile_id:
|
|
80
|
+
raise ObligationConflictError(
|
|
81
|
+
f"obligation {context.operation_id}/{owner}/{kind} "
|
|
82
|
+
"already recorded for a different profile"
|
|
83
|
+
)
|
|
84
|
+
if stored is None:
|
|
85
|
+
conn.execute(
|
|
86
|
+
"UPDATE projection_obligations SET context_digest = ? "
|
|
87
|
+
"WHERE operation_id = ? AND owner = ? AND kind = ?",
|
|
88
|
+
(digest, context.operation_id, owner, str(kind)),
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
if stored != digest:
|
|
92
|
+
raise ObligationConflictError(
|
|
93
|
+
f"obligation {context.operation_id}/{owner}/{kind} "
|
|
94
|
+
"already recorded with a different context"
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
now = time.time()
|
|
98
|
+
conn.execute(
|
|
99
|
+
"INSERT INTO projection_obligations "
|
|
100
|
+
"(operation_id, profile_id, owner, kind, subject_id, revision, "
|
|
101
|
+
"state, checksum, detail, attempts, context_digest, "
|
|
102
|
+
"verify_attempts, last_verified_at, created_at, updated_at) "
|
|
103
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0, ?, 0, NULL, ?, ?)",
|
|
104
|
+
(
|
|
105
|
+
context.operation_id,
|
|
106
|
+
context.profile_id,
|
|
107
|
+
owner,
|
|
108
|
+
str(kind),
|
|
109
|
+
context.subject_id,
|
|
110
|
+
context.revision,
|
|
111
|
+
str(ObligationState.PENDING),
|
|
112
|
+
digest,
|
|
113
|
+
now,
|
|
114
|
+
now,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def record_many(
|
|
119
|
+
self,
|
|
120
|
+
conn: sqlite3.Connection,
|
|
121
|
+
context: OperationContext,
|
|
122
|
+
owners: Iterable[str],
|
|
123
|
+
kind: ObligationKind,
|
|
124
|
+
) -> None:
|
|
125
|
+
for owner in owners:
|
|
126
|
+
self.record(conn, context, owner, kind)
|
|
127
|
+
|
|
128
|
+
def mark(
|
|
129
|
+
self,
|
|
130
|
+
conn: sqlite3.Connection,
|
|
131
|
+
operation_id: str,
|
|
132
|
+
owner: str,
|
|
133
|
+
kind: ObligationKind,
|
|
134
|
+
state: ObligationState,
|
|
135
|
+
*,
|
|
136
|
+
checksum: str | None = None,
|
|
137
|
+
detail: Mapping[str, Any] | None = None,
|
|
138
|
+
bump_attempts: bool = False,
|
|
139
|
+
bump_verify_attempts: bool = False,
|
|
140
|
+
set_verified_at: bool = False,
|
|
141
|
+
) -> int:
|
|
142
|
+
detail_json = None if detail is None else json.dumps(
|
|
143
|
+
dict(detail), sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
144
|
+
)
|
|
145
|
+
now = time.time()
|
|
146
|
+
cursor = conn.execute(
|
|
147
|
+
"UPDATE projection_obligations SET "
|
|
148
|
+
"state = ?, "
|
|
149
|
+
"checksum = COALESCE(?, checksum), "
|
|
150
|
+
"detail = ?, "
|
|
151
|
+
"attempts = attempts + ?, "
|
|
152
|
+
"verify_attempts = verify_attempts + ?, "
|
|
153
|
+
"last_verified_at = CASE WHEN ? THEN ? ELSE last_verified_at END, "
|
|
154
|
+
"updated_at = ? "
|
|
155
|
+
"WHERE operation_id = ? AND owner = ? AND kind = ?",
|
|
156
|
+
(
|
|
157
|
+
str(state),
|
|
158
|
+
checksum,
|
|
159
|
+
detail_json,
|
|
160
|
+
1 if bump_attempts else 0,
|
|
161
|
+
1 if bump_verify_attempts else 0,
|
|
162
|
+
1 if set_verified_at else 0,
|
|
163
|
+
now,
|
|
164
|
+
now,
|
|
165
|
+
operation_id,
|
|
166
|
+
owner,
|
|
167
|
+
str(kind),
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
return cursor.rowcount
|
|
171
|
+
|
|
172
|
+
def fetch(
|
|
173
|
+
self, conn: sqlite3.Connection, operation_id: str,
|
|
174
|
+
) -> tuple[Obligation, ...]:
|
|
175
|
+
rows = conn.execute(
|
|
176
|
+
f"SELECT {_SELECT_COLUMNS} FROM projection_obligations "
|
|
177
|
+
"WHERE operation_id = ? ORDER BY owner, kind",
|
|
178
|
+
(operation_id,),
|
|
179
|
+
).fetchall()
|
|
180
|
+
return tuple(_row_to_obligation(row) for row in rows)
|
|
181
|
+
|
|
182
|
+
def fetch_by_state(
|
|
183
|
+
self,
|
|
184
|
+
conn: sqlite3.Connection,
|
|
185
|
+
state: ObligationState,
|
|
186
|
+
*,
|
|
187
|
+
limit: int = 100,
|
|
188
|
+
) -> tuple[Obligation, ...]:
|
|
189
|
+
rows = conn.execute(
|
|
190
|
+
f"SELECT {_SELECT_COLUMNS} FROM projection_obligations "
|
|
191
|
+
"WHERE state = ? ORDER BY updated_at LIMIT ?",
|
|
192
|
+
(str(state), limit),
|
|
193
|
+
).fetchall()
|
|
194
|
+
return tuple(_row_to_obligation(row) for row in rows)
|
|
195
|
+
|
|
196
|
+
def pending_operation_ids(
|
|
197
|
+
self,
|
|
198
|
+
conn: sqlite3.Connection,
|
|
199
|
+
*,
|
|
200
|
+
profile_id: str | None = None,
|
|
201
|
+
limit: int = 100,
|
|
202
|
+
max_attempts: int = 10,
|
|
203
|
+
) -> tuple[str, ...]:
|
|
204
|
+
where = "state NOT IN (?, ?) AND attempts < ?"
|
|
205
|
+
params: list[Any] = [
|
|
206
|
+
str(ObligationState.VERIFIED), str(ObligationState.ERASED), max_attempts,
|
|
207
|
+
]
|
|
208
|
+
if profile_id is not None:
|
|
209
|
+
where += " AND profile_id = ?"
|
|
210
|
+
params.append(profile_id)
|
|
211
|
+
params.append(limit)
|
|
212
|
+
rows = conn.execute(
|
|
213
|
+
f"SELECT DISTINCT operation_id FROM projection_obligations "
|
|
214
|
+
f"WHERE {where} ORDER BY updated_at LIMIT ?",
|
|
215
|
+
tuple(params),
|
|
216
|
+
).fetchall()
|
|
217
|
+
return tuple(row[0] for row in rows)
|
|
218
|
+
|
|
219
|
+
def operations_missing_manifest(
|
|
220
|
+
self,
|
|
221
|
+
conn: sqlite3.Connection,
|
|
222
|
+
*,
|
|
223
|
+
profile_id: str | None = None,
|
|
224
|
+
limit: int = 100,
|
|
225
|
+
max_attempts: int = 10,
|
|
226
|
+
) -> tuple[str, ...]:
|
|
227
|
+
where = "m.operation_id IS NULL AND o.attempts < ?"
|
|
228
|
+
params: list[Any] = [max_attempts]
|
|
229
|
+
if profile_id is not None:
|
|
230
|
+
where += " AND o.profile_id = ?"
|
|
231
|
+
params.append(profile_id)
|
|
232
|
+
params.append(limit)
|
|
233
|
+
rows = conn.execute(
|
|
234
|
+
f"SELECT DISTINCT o.operation_id FROM projection_obligations o "
|
|
235
|
+
f"LEFT JOIN completion_manifests m ON m.operation_id = o.operation_id "
|
|
236
|
+
f"WHERE {where} ORDER BY o.operation_id LIMIT ?",
|
|
237
|
+
tuple(params),
|
|
238
|
+
).fetchall()
|
|
239
|
+
return tuple(row[0] for row in rows)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _row_to_obligation(row: Any) -> Obligation:
|
|
243
|
+
detail_raw = row[8]
|
|
244
|
+
detail: Mapping[str, Any] | None
|
|
245
|
+
if detail_raw is None:
|
|
246
|
+
detail = None
|
|
247
|
+
else:
|
|
248
|
+
try:
|
|
249
|
+
parsed = json.loads(detail_raw)
|
|
250
|
+
detail = parsed if isinstance(parsed, dict) else {"value": parsed}
|
|
251
|
+
except (TypeError, json.JSONDecodeError):
|
|
252
|
+
detail = None
|
|
253
|
+
return Obligation(
|
|
254
|
+
operation_id=row[0],
|
|
255
|
+
profile_id=row[1],
|
|
256
|
+
owner=row[2],
|
|
257
|
+
kind=ObligationKind(row[3]),
|
|
258
|
+
subject_id=row[4],
|
|
259
|
+
revision=int(row[5]),
|
|
260
|
+
state=ObligationState(row[6]),
|
|
261
|
+
checksum=row[7],
|
|
262
|
+
detail=detail,
|
|
263
|
+
attempts=int(row[9]),
|
|
264
|
+
context_digest=row[10],
|
|
265
|
+
verify_attempts=int(row[11]) if row[11] is not None else 0,
|
|
266
|
+
last_verified_at=float(row[12]) if row[12] is not None else None,
|
|
267
|
+
created_at=float(row[13]),
|
|
268
|
+
updated_at=float(row[14]),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
__all__ = ["Obligation", "ObligationConflictError", "ObligationLedger", "context_digest"]
|