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,542 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""OperationPolicyRegistry — declarative admission / policy layer (V4 Phase 4).
|
|
5
|
+
|
|
6
|
+
Design contract (NON-NEGOTIABLE):
|
|
7
|
+
1. evaluate() is PURE and CPU-ONLY. No file, network, or DB access. Microseconds.
|
|
8
|
+
2. Default policy is ADDITIVE: every operation kind that succeeds today continues
|
|
9
|
+
to succeed after this module is imported. The registry adds checks on top of
|
|
10
|
+
the existing trust hook + RBAC + ingest gate — it does NOT replace them.
|
|
11
|
+
3. Unknown kind: fail-OPEN in local/single-user mode (annotate with audit=True).
|
|
12
|
+
fail-CLOSED in company/remote mode (deny with reason string).
|
|
13
|
+
4. Payload > max_payload_bytes: ANNOTATED only — never rejected here. The ingest
|
|
14
|
+
gate already enforces the 1 MiB hard cap; double-rejection is forbidden.
|
|
15
|
+
5. The module-level _DEFAULT_REGISTRY singleton is the safe default for all
|
|
16
|
+
call sites. It never rejects any REMEMBER that the existing stack accepts.
|
|
17
|
+
|
|
18
|
+
Part of SuperLocalMemory V4 | Phase 4: Admission/Policy Layer
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import types
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
|
|
26
|
+
from superlocalmemory.core.actor_context import ActorContext, ActorRole, Transport
|
|
27
|
+
from superlocalmemory.core.operation_policy import (
|
|
28
|
+
_ADMIN_TRANSPORTS,
|
|
29
|
+
_ALL_TRANSPORTS,
|
|
30
|
+
_MESH_TRANSPORTS,
|
|
31
|
+
OperationPolicy,
|
|
32
|
+
)
|
|
33
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Mode sentinel sets — used by evaluate() to decide unknown-kind behaviour.
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
_LOCAL_MODES: frozenset[str] = frozenset({
|
|
40
|
+
"local", "single-user", "single_user", "personal",
|
|
41
|
+
})
|
|
42
|
+
_COMPANY_MODES: frozenset[str] = frozenset({
|
|
43
|
+
"company", "remote", "multi-user", "multi_user", "enterprise",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# PolicyDecision — the return value of evaluate()
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class PolicyDecision:
|
|
53
|
+
"""Result of one OperationPolicyRegistry.evaluate() call.
|
|
54
|
+
|
|
55
|
+
``allowed`` Whether the operation is admitted by policy.
|
|
56
|
+
``reason`` Machine-readable reason code (never a user-visible message).
|
|
57
|
+
``annotations`` Supplementary key/value pairs for audit, telemetry, and
|
|
58
|
+
downstream enrichment. Exposed as a read-only mapping so a
|
|
59
|
+
frozen decision cannot be mutated after construction.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
allowed: bool
|
|
63
|
+
reason: str
|
|
64
|
+
annotations: dict = field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
def __post_init__(self) -> None:
|
|
67
|
+
# Freeze the annotations mapping so the whole decision is immutable
|
|
68
|
+
# (the outer dataclass is frozen; this closes the nested-dict hole).
|
|
69
|
+
from types import MappingProxyType
|
|
70
|
+
|
|
71
|
+
if not isinstance(self.annotations, MappingProxyType):
|
|
72
|
+
object.__setattr__(
|
|
73
|
+
self, "annotations", MappingProxyType(dict(self.annotations))
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# OperationPolicyRegistry
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class OperationPolicyRegistry:
|
|
83
|
+
"""Declarative, immutable registry mapping OperationKind → OperationPolicy.
|
|
84
|
+
|
|
85
|
+
Construction
|
|
86
|
+
------------
|
|
87
|
+
Use ``OperationPolicyRegistry.default()`` to obtain the safe production
|
|
88
|
+
default. All known OperationKind values are pre-populated with policies
|
|
89
|
+
that preserve the current single-user allow behaviour.
|
|
90
|
+
|
|
91
|
+
Immutability
|
|
92
|
+
------------
|
|
93
|
+
The internal policy table is wrapped in ``types.MappingProxyType`` to
|
|
94
|
+
prevent external mutation. ``register_deny`` and ``with_policy`` return
|
|
95
|
+
NEW registry instances (immutable-update pattern).
|
|
96
|
+
|
|
97
|
+
evaluate() contract
|
|
98
|
+
-------------------
|
|
99
|
+
- Pure: no I/O, no global mutation, no side effects.
|
|
100
|
+
- Fast: dict lookup + frozenset.isdisjoint() + field comparisons.
|
|
101
|
+
- Thread-safe: all state is read-only after construction.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
policies: dict[OperationKind, OperationPolicy],
|
|
107
|
+
explicit_denies: frozenset[OperationKind] = frozenset(),
|
|
108
|
+
) -> None:
|
|
109
|
+
# MappingProxyType enforces read-only access after construction.
|
|
110
|
+
self._policies: types.MappingProxyType[
|
|
111
|
+
OperationKind, OperationPolicy
|
|
112
|
+
] = types.MappingProxyType(dict(policies))
|
|
113
|
+
self._explicit_denies: frozenset[OperationKind] = frozenset(explicit_denies)
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# Constructors
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def default(cls) -> OperationPolicyRegistry:
|
|
121
|
+
"""Build the production-default registry.
|
|
122
|
+
|
|
123
|
+
Every known OperationKind is assigned a policy whose required_roles
|
|
124
|
+
includes ActorRole.OWNER and allowed_transports includes
|
|
125
|
+
Transport.INTERNAL — so the existing in-process Python API path and the
|
|
126
|
+
single-user HTTP path continue to be admitted without any new rejection.
|
|
127
|
+
|
|
128
|
+
Policy table rationale
|
|
129
|
+
----------------------
|
|
130
|
+
REMEMBER / RECALL Core R/W — open to OWNER, ADMIN, MEMBER.
|
|
131
|
+
FORGET / CORRECT Reversible mutations — OWNER and ADMIN only.
|
|
132
|
+
ERASE Irreversible — OWNER only.
|
|
133
|
+
CONSOLIDATE / ARCHIVE Maintenance — OWNER and ADMIN.
|
|
134
|
+
BACKUP / RESTORE_BACKUP Administrative — OWNER only, any transport.
|
|
135
|
+
MESH_* Collaboration — OWNER, ADMIN, MEMBER; mesh transports.
|
|
136
|
+
PROVIDER_TEST Internal probing — OWNER and ADMIN; CLI/INTERNAL only.
|
|
137
|
+
MODE_CHANGE System config — OWNER only; admin transports only.
|
|
138
|
+
PROFILE_SWITCH Profile ops — OWNER and ADMIN; all transports.
|
|
139
|
+
SCHEMA_MIGRATE Dangerous DDL — OWNER only; CLI/INTERNAL only.
|
|
140
|
+
VECTOR_MIGRATE Index DDL — OWNER and ADMIN; CLI/INTERNAL only.
|
|
141
|
+
"""
|
|
142
|
+
_owner_only = frozenset({ActorRole.OWNER})
|
|
143
|
+
_owner_admin = frozenset({ActorRole.OWNER, ActorRole.ADMIN})
|
|
144
|
+
_owner_admin_member = frozenset({
|
|
145
|
+
ActorRole.OWNER, ActorRole.ADMIN, ActorRole.MEMBER,
|
|
146
|
+
})
|
|
147
|
+
_all_reads = frozenset({
|
|
148
|
+
ActorRole.OWNER, ActorRole.ADMIN, ActorRole.MEMBER, ActorRole.VIEWER,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
policies: dict[OperationKind, OperationPolicy] = {
|
|
152
|
+
OperationKind.REMEMBER: OperationPolicy(
|
|
153
|
+
kind=OperationKind.REMEMBER,
|
|
154
|
+
required_roles=_owner_admin_member,
|
|
155
|
+
required_authentication=True,
|
|
156
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
157
|
+
audit_level="standard",
|
|
158
|
+
),
|
|
159
|
+
OperationKind.RECALL: OperationPolicy(
|
|
160
|
+
kind=OperationKind.RECALL,
|
|
161
|
+
required_roles=_all_reads,
|
|
162
|
+
required_authentication=True,
|
|
163
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
164
|
+
audit_level="none",
|
|
165
|
+
),
|
|
166
|
+
OperationKind.FORGET: OperationPolicy(
|
|
167
|
+
kind=OperationKind.FORGET,
|
|
168
|
+
required_roles=_owner_admin,
|
|
169
|
+
required_authentication=True,
|
|
170
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
171
|
+
audit_level="full",
|
|
172
|
+
),
|
|
173
|
+
OperationKind.ARCHIVE: OperationPolicy(
|
|
174
|
+
kind=OperationKind.ARCHIVE,
|
|
175
|
+
required_roles=_owner_admin,
|
|
176
|
+
required_authentication=True,
|
|
177
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
178
|
+
audit_level="standard",
|
|
179
|
+
),
|
|
180
|
+
OperationKind.RESTORE: OperationPolicy(
|
|
181
|
+
kind=OperationKind.RESTORE,
|
|
182
|
+
required_roles=_owner_admin,
|
|
183
|
+
required_authentication=True,
|
|
184
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
185
|
+
audit_level="standard",
|
|
186
|
+
),
|
|
187
|
+
OperationKind.CORRECT: OperationPolicy(
|
|
188
|
+
kind=OperationKind.CORRECT,
|
|
189
|
+
required_roles=_owner_admin,
|
|
190
|
+
required_authentication=True,
|
|
191
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
192
|
+
audit_level="full",
|
|
193
|
+
),
|
|
194
|
+
OperationKind.ERASE: OperationPolicy(
|
|
195
|
+
kind=OperationKind.ERASE,
|
|
196
|
+
required_roles=_owner_only,
|
|
197
|
+
required_authentication=True,
|
|
198
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
199
|
+
audit_level="full",
|
|
200
|
+
redaction_policy="full",
|
|
201
|
+
resource_ownership_check=True,
|
|
202
|
+
),
|
|
203
|
+
OperationKind.CONSOLIDATE: OperationPolicy(
|
|
204
|
+
kind=OperationKind.CONSOLIDATE,
|
|
205
|
+
required_roles=_owner_admin,
|
|
206
|
+
required_authentication=True,
|
|
207
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
208
|
+
audit_level="standard",
|
|
209
|
+
),
|
|
210
|
+
OperationKind.BACKUP: OperationPolicy(
|
|
211
|
+
kind=OperationKind.BACKUP,
|
|
212
|
+
required_roles=_owner_only,
|
|
213
|
+
required_authentication=True,
|
|
214
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
215
|
+
audit_level="standard",
|
|
216
|
+
),
|
|
217
|
+
OperationKind.RESTORE_BACKUP: OperationPolicy(
|
|
218
|
+
kind=OperationKind.RESTORE_BACKUP,
|
|
219
|
+
required_roles=_owner_only,
|
|
220
|
+
required_authentication=True,
|
|
221
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
222
|
+
audit_level="full",
|
|
223
|
+
),
|
|
224
|
+
OperationKind.MESH_SEND: OperationPolicy(
|
|
225
|
+
kind=OperationKind.MESH_SEND,
|
|
226
|
+
required_roles=_owner_admin_member,
|
|
227
|
+
required_authentication=True,
|
|
228
|
+
allowed_transports=_MESH_TRANSPORTS,
|
|
229
|
+
audit_level="standard",
|
|
230
|
+
),
|
|
231
|
+
OperationKind.MESH_LOCK: OperationPolicy(
|
|
232
|
+
kind=OperationKind.MESH_LOCK,
|
|
233
|
+
required_roles=_owner_admin,
|
|
234
|
+
required_authentication=True,
|
|
235
|
+
allowed_transports=frozenset({
|
|
236
|
+
Transport.MESH, Transport.INTERNAL, Transport.MCP,
|
|
237
|
+
}),
|
|
238
|
+
audit_level="full",
|
|
239
|
+
),
|
|
240
|
+
OperationKind.PROVIDER_TEST: OperationPolicy(
|
|
241
|
+
kind=OperationKind.PROVIDER_TEST,
|
|
242
|
+
required_roles=_owner_admin,
|
|
243
|
+
required_authentication=True,
|
|
244
|
+
allowed_transports=frozenset({
|
|
245
|
+
Transport.CLI, Transport.INTERNAL, Transport.DASHBOARD,
|
|
246
|
+
}),
|
|
247
|
+
audit_level="standard",
|
|
248
|
+
),
|
|
249
|
+
OperationKind.MODE_CHANGE: OperationPolicy(
|
|
250
|
+
kind=OperationKind.MODE_CHANGE,
|
|
251
|
+
required_roles=_owner_only,
|
|
252
|
+
required_authentication=True,
|
|
253
|
+
allowed_transports=_ADMIN_TRANSPORTS | frozenset({Transport.MCP}),
|
|
254
|
+
audit_level="full",
|
|
255
|
+
),
|
|
256
|
+
OperationKind.PROFILE_SWITCH: OperationPolicy(
|
|
257
|
+
kind=OperationKind.PROFILE_SWITCH,
|
|
258
|
+
required_roles=_owner_admin,
|
|
259
|
+
required_authentication=True,
|
|
260
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
261
|
+
audit_level="standard",
|
|
262
|
+
),
|
|
263
|
+
OperationKind.SCHEMA_MIGRATE: OperationPolicy(
|
|
264
|
+
kind=OperationKind.SCHEMA_MIGRATE,
|
|
265
|
+
required_roles=_owner_only,
|
|
266
|
+
required_authentication=True,
|
|
267
|
+
allowed_transports=frozenset({Transport.CLI, Transport.INTERNAL}),
|
|
268
|
+
audit_level="full",
|
|
269
|
+
),
|
|
270
|
+
OperationKind.VECTOR_MIGRATE: OperationPolicy(
|
|
271
|
+
kind=OperationKind.VECTOR_MIGRATE,
|
|
272
|
+
required_roles=_owner_admin,
|
|
273
|
+
required_authentication=True,
|
|
274
|
+
allowed_transports=frozenset({Transport.CLI, Transport.INTERNAL}),
|
|
275
|
+
audit_level="full",
|
|
276
|
+
),
|
|
277
|
+
OperationKind.EVOLVE_SKILL: OperationPolicy(
|
|
278
|
+
kind=OperationKind.EVOLVE_SKILL,
|
|
279
|
+
required_roles=_owner_admin,
|
|
280
|
+
required_authentication=True,
|
|
281
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
282
|
+
audit_level="full",
|
|
283
|
+
),
|
|
284
|
+
# Operational recovery & admin remediation (Wave-3 resilience slice)
|
|
285
|
+
# OPS_INSPECT: read-only listing of failed/stuck/degraded ops.
|
|
286
|
+
# Allowed over all transports so dashboard, MCP, and CLI all work.
|
|
287
|
+
OperationKind.OPS_INSPECT: OperationPolicy(
|
|
288
|
+
kind=OperationKind.OPS_INSPECT,
|
|
289
|
+
required_roles=_owner_admin,
|
|
290
|
+
required_authentication=True,
|
|
291
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
292
|
+
audit_level="standard",
|
|
293
|
+
),
|
|
294
|
+
# OPS_RESOLVE: retry/force-reconcile/cancel a failed operation.
|
|
295
|
+
# Write permission — OWNER/ADMIN only, full audit trail.
|
|
296
|
+
OperationKind.OPS_RESOLVE: OperationPolicy(
|
|
297
|
+
kind=OperationKind.OPS_RESOLVE,
|
|
298
|
+
required_roles=_owner_admin,
|
|
299
|
+
required_authentication=True,
|
|
300
|
+
allowed_transports=_ALL_TRANSPORTS,
|
|
301
|
+
audit_level="full",
|
|
302
|
+
),
|
|
303
|
+
}
|
|
304
|
+
return cls(policies)
|
|
305
|
+
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
# Immutable-update helpers
|
|
308
|
+
# ------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def with_policy(self, policy: OperationPolicy) -> OperationPolicyRegistry:
|
|
311
|
+
"""Return a new registry with one policy replaced or added."""
|
|
312
|
+
updated = dict(self._policies)
|
|
313
|
+
updated[policy.kind] = policy
|
|
314
|
+
return OperationPolicyRegistry(updated, self._explicit_denies)
|
|
315
|
+
|
|
316
|
+
def register_deny(self, kind: OperationKind) -> OperationPolicyRegistry:
|
|
317
|
+
"""Return a new registry that explicitly denies ``kind`` regardless of actor."""
|
|
318
|
+
return OperationPolicyRegistry(
|
|
319
|
+
dict(self._policies),
|
|
320
|
+
self._explicit_denies | {kind},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# ------------------------------------------------------------------
|
|
324
|
+
# Queries
|
|
325
|
+
# ------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
def coverage(self) -> dict[str, dict]:
|
|
328
|
+
"""Return a coverage summary for every known OperationKind.
|
|
329
|
+
|
|
330
|
+
Returns a mapping of {kind_value: {"has_policy": bool}} for every
|
|
331
|
+
value in OperationKind. Used by the startup self-check and unit tests.
|
|
332
|
+
|
|
333
|
+
Example::
|
|
334
|
+
|
|
335
|
+
cov = _DEFAULT_REGISTRY.coverage()
|
|
336
|
+
assert cov["remember"]["has_policy"] is True
|
|
337
|
+
"""
|
|
338
|
+
result: dict[str, dict] = {}
|
|
339
|
+
for kind in OperationKind:
|
|
340
|
+
has_policy = kind in self._policies
|
|
341
|
+
entry: dict = {"has_policy": has_policy, "kind": kind.value}
|
|
342
|
+
if has_policy:
|
|
343
|
+
policy = self._policies[kind]
|
|
344
|
+
entry["has_transports"] = len(policy.allowed_transports) > 0
|
|
345
|
+
else:
|
|
346
|
+
entry["has_transports"] = False
|
|
347
|
+
result[kind.value] = entry
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
def get_policy(
|
|
351
|
+
self,
|
|
352
|
+
kind: OperationKind,
|
|
353
|
+
transport: Transport | None = None, # reserved for future per-transport overrides
|
|
354
|
+
) -> OperationPolicy | None:
|
|
355
|
+
"""Return the policy for ``kind``, or None if no policy is registered.
|
|
356
|
+
|
|
357
|
+
The ``transport`` argument is reserved for future per-transport policy
|
|
358
|
+
overrides. In V4 the table is keyed only by kind; the argument is
|
|
359
|
+
accepted but not used for lookup.
|
|
360
|
+
"""
|
|
361
|
+
return self._policies.get(kind)
|
|
362
|
+
|
|
363
|
+
# ------------------------------------------------------------------
|
|
364
|
+
# Core: pure, CPU-only evaluation
|
|
365
|
+
# ------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
def evaluate(
|
|
368
|
+
self,
|
|
369
|
+
kind: OperationKind | str,
|
|
370
|
+
actor: ActorContext,
|
|
371
|
+
mode: str = "local",
|
|
372
|
+
*,
|
|
373
|
+
payload_bytes: int = 0,
|
|
374
|
+
) -> PolicyDecision:
|
|
375
|
+
"""Evaluate whether ``actor`` may perform ``kind`` under ``mode``.
|
|
376
|
+
|
|
377
|
+
This method is PURE. No I/O of any kind. Microseconds.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
kind The operation being requested. May be an OperationKind
|
|
382
|
+
enum value or a raw string (unknown strings are treated
|
|
383
|
+
as unknown kinds).
|
|
384
|
+
actor Server-derived actor identity. NEVER from request body.
|
|
385
|
+
mode Deployment mode: "local" / "single-user" → fail-open for
|
|
386
|
+
unknown kinds. "company" / "remote" → fail-closed.
|
|
387
|
+
Defaults to "local" (safe for internal path).
|
|
388
|
+
payload_bytes Optional advisory payload size. If > policy.max_payload_bytes,
|
|
389
|
+
the decision is annotated but NOT denied (double-rejection
|
|
390
|
+
is forbidden — the ingest gate already enforces the cap).
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
PolicyDecision ``allowed=True`` → proceed; ``allowed=False`` → deny.
|
|
395
|
+
Never raises; all errors produce ``allowed=False``.
|
|
396
|
+
"""
|
|
397
|
+
annotations: dict = {}
|
|
398
|
+
|
|
399
|
+
# ----------------------------------------------------------------
|
|
400
|
+
# Step 1: Resolve kind string → OperationKind enum.
|
|
401
|
+
# ----------------------------------------------------------------
|
|
402
|
+
if not isinstance(kind, OperationKind):
|
|
403
|
+
try:
|
|
404
|
+
kind = OperationKind(str(kind))
|
|
405
|
+
except ValueError:
|
|
406
|
+
return self._unknown_kind_decision(str(kind), mode)
|
|
407
|
+
|
|
408
|
+
# ----------------------------------------------------------------
|
|
409
|
+
# Step 2: Check explicit deny list (highest priority).
|
|
410
|
+
# ----------------------------------------------------------------
|
|
411
|
+
if kind in self._explicit_denies:
|
|
412
|
+
return PolicyDecision(
|
|
413
|
+
allowed=False,
|
|
414
|
+
reason="explicit_deny",
|
|
415
|
+
annotations={"kind": kind.value},
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# ----------------------------------------------------------------
|
|
419
|
+
# Step 3: Look up policy for this kind.
|
|
420
|
+
# ----------------------------------------------------------------
|
|
421
|
+
policy = self._policies.get(kind)
|
|
422
|
+
if policy is None:
|
|
423
|
+
# Known enum value but no policy registered — treat as unknown.
|
|
424
|
+
return self._unknown_kind_decision(kind.value, mode)
|
|
425
|
+
|
|
426
|
+
# ----------------------------------------------------------------
|
|
427
|
+
# Step 4: Role check — actor must hold at least one required role.
|
|
428
|
+
# An empty required_roles set means "deny all" (explicit deny policy).
|
|
429
|
+
# ----------------------------------------------------------------
|
|
430
|
+
if not policy.required_roles:
|
|
431
|
+
return PolicyDecision(
|
|
432
|
+
allowed=False,
|
|
433
|
+
reason="policy_denies_all_roles",
|
|
434
|
+
annotations={"kind": kind.value},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# ----------------------------------------------------------------
|
|
438
|
+
# Step 5: Authentication before authorization — an unauthenticated
|
|
439
|
+
# actor is rejected for authentication, not role, so the reason is
|
|
440
|
+
# actionable.
|
|
441
|
+
# ----------------------------------------------------------------
|
|
442
|
+
if policy.required_authentication and not actor.is_authenticated:
|
|
443
|
+
return PolicyDecision(
|
|
444
|
+
allowed=False,
|
|
445
|
+
reason="authentication_required",
|
|
446
|
+
annotations={"kind": kind.value, "principal_id": actor.principal_id},
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# ----------------------------------------------------------------
|
|
450
|
+
# Step 6: Role check — actor must hold at least one required role.
|
|
451
|
+
# ----------------------------------------------------------------
|
|
452
|
+
if actor.roles.isdisjoint(policy.required_roles):
|
|
453
|
+
return PolicyDecision(
|
|
454
|
+
allowed=False,
|
|
455
|
+
reason="insufficient_roles",
|
|
456
|
+
annotations={
|
|
457
|
+
"kind": kind.value,
|
|
458
|
+
"actor_roles": sorted(r.value for r in actor.roles),
|
|
459
|
+
"required_roles": sorted(r.value for r in policy.required_roles),
|
|
460
|
+
},
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# ----------------------------------------------------------------
|
|
464
|
+
# Step 7: Transport check.
|
|
465
|
+
# ----------------------------------------------------------------
|
|
466
|
+
if actor.transport not in policy.allowed_transports:
|
|
467
|
+
return PolicyDecision(
|
|
468
|
+
allowed=False,
|
|
469
|
+
reason="transport_not_allowed",
|
|
470
|
+
annotations={
|
|
471
|
+
"kind": kind.value,
|
|
472
|
+
"actor_transport": actor.transport.value,
|
|
473
|
+
"allowed_transports": sorted(
|
|
474
|
+
t.value for t in policy.allowed_transports
|
|
475
|
+
),
|
|
476
|
+
},
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# ----------------------------------------------------------------
|
|
480
|
+
# Step 7: Payload size — annotate only, never reject.
|
|
481
|
+
# The ingest gate already enforces the 1 MiB hard cap. Annotating
|
|
482
|
+
# here gives the audit log a signal without double-rejecting.
|
|
483
|
+
# ----------------------------------------------------------------
|
|
484
|
+
if payload_bytes > 0 and payload_bytes > policy.max_payload_bytes:
|
|
485
|
+
annotations["payload_oversized"] = True
|
|
486
|
+
annotations["payload_bytes"] = payload_bytes
|
|
487
|
+
annotations["max_payload_bytes"] = policy.max_payload_bytes
|
|
488
|
+
|
|
489
|
+
# ----------------------------------------------------------------
|
|
490
|
+
# Step 8: Admit.
|
|
491
|
+
# ----------------------------------------------------------------
|
|
492
|
+
return PolicyDecision(
|
|
493
|
+
allowed=True,
|
|
494
|
+
reason="allow",
|
|
495
|
+
annotations=annotations,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# ------------------------------------------------------------------
|
|
499
|
+
# Private helpers
|
|
500
|
+
# ------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
def _unknown_kind_decision(self, kind_raw: str, mode: str) -> PolicyDecision:
|
|
503
|
+
"""Fail-open in local mode, fail-closed in company/remote mode."""
|
|
504
|
+
if mode in _LOCAL_MODES:
|
|
505
|
+
return PolicyDecision(
|
|
506
|
+
allowed=True,
|
|
507
|
+
reason="unknown_kind_allow_local",
|
|
508
|
+
annotations={"audit": True, "kind_raw": kind_raw},
|
|
509
|
+
)
|
|
510
|
+
# Company / remote mode: fail closed. An unknown kind may be a
|
|
511
|
+
# future privileged operation or a typo from an untrusted caller.
|
|
512
|
+
return PolicyDecision(
|
|
513
|
+
allowed=False,
|
|
514
|
+
reason="unknown_kind_deny_company",
|
|
515
|
+
annotations={"kind_raw": kind_raw},
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
def __repr__(self) -> str:
|
|
519
|
+
return (
|
|
520
|
+
f"OperationPolicyRegistry("
|
|
521
|
+
f"kinds={sorted(k.value for k in self._policies)}, "
|
|
522
|
+
f"explicit_denies={sorted(k.value for k in self._explicit_denies)})"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
# Module-level singleton — safe default for all wire-up call sites.
|
|
528
|
+
#
|
|
529
|
+
# CRIT: This singleton is constructed ONCE at import time. It is read-only
|
|
530
|
+
# (MappingProxyType internal storage). Thread-safe by construction.
|
|
531
|
+
# The default evaluate() for REMEMBER + OWNER + local ALWAYS returns
|
|
532
|
+
# allowed=True — zero new rejections on the existing happy path.
|
|
533
|
+
# ---------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
_DEFAULT_REGISTRY: OperationPolicyRegistry = OperationPolicyRegistry.default()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
__all__ = [
|
|
539
|
+
"OperationPolicyRegistry",
|
|
540
|
+
"PolicyDecision",
|
|
541
|
+
"_DEFAULT_REGISTRY",
|
|
542
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Admission envelope (OperationRequest) and status types for V4.
|
|
5
|
+
|
|
6
|
+
OperationRequest is the typed, immutable boundary between every caller and the
|
|
7
|
+
policy/ingestion layer. The registry uses only the fields it can evaluate
|
|
8
|
+
purely in memory (kind, actor, scope, payload_hash) — no I/O.
|
|
9
|
+
|
|
10
|
+
Part of SuperLocalMemory V4 | Phase 4: Admission/Policy Layer
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import uuid
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from superlocalmemory.core.actor_context import ActorContext, ActorRole, Transport
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
pass # reserved for future typing imports
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OperationKind(str, Enum):
|
|
28
|
+
"""Canonical names for every mutation kind the system accepts.
|
|
29
|
+
|
|
30
|
+
The registry stores one OperationPolicy per OperationKind. An operation
|
|
31
|
+
whose kind is absent from the registry is treated as "unknown" and
|
|
32
|
+
evaluated by the mode-specific fallback rule (fail-open local /
|
|
33
|
+
fail-closed company).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
REMEMBER = "remember"
|
|
37
|
+
RECALL = "recall"
|
|
38
|
+
FORGET = "forget"
|
|
39
|
+
ARCHIVE = "archive"
|
|
40
|
+
RESTORE = "restore"
|
|
41
|
+
CORRECT = "correct"
|
|
42
|
+
ERASE = "erase"
|
|
43
|
+
CONSOLIDATE = "consolidate"
|
|
44
|
+
BACKUP = "backup"
|
|
45
|
+
RESTORE_BACKUP = "restore_backup"
|
|
46
|
+
MESH_SEND = "mesh_send"
|
|
47
|
+
MESH_LOCK = "mesh_lock"
|
|
48
|
+
PROVIDER_TEST = "provider_test"
|
|
49
|
+
MODE_CHANGE = "mode_change"
|
|
50
|
+
PROFILE_SWITCH = "profile_switch"
|
|
51
|
+
SCHEMA_MIGRATE = "schema_migrate"
|
|
52
|
+
VECTOR_MIGRATE = "vector_migrate"
|
|
53
|
+
EVOLVE_SKILL = "evolve_skill"
|
|
54
|
+
# Operational recovery & admin remediation (V4 Wave-3 resilience slice)
|
|
55
|
+
OPS_INSPECT = "ops_inspect" # List failed/stuck/degraded operations (OWNER/ADMIN)
|
|
56
|
+
OPS_RESOLVE = "ops_resolve" # Retry/force-reconcile/cancel an operation (OWNER/ADMIN)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OperationStatus(str, Enum):
|
|
60
|
+
"""Lifecycle states for a durable operation."""
|
|
61
|
+
|
|
62
|
+
ACCEPTED = "accepted" # Journaled, not yet committed
|
|
63
|
+
COMMITTED = "committed" # Written to canonical store
|
|
64
|
+
PROJECTING = "projecting" # Running external projections
|
|
65
|
+
COMPLETE = "complete" # All obligations met
|
|
66
|
+
DEGRADED = "degraded" # Some projections failed, retryable
|
|
67
|
+
FAILED = "failed" # Non-retryable failure
|
|
68
|
+
ROLLED_BACK = "rolled_back" # Compensation complete
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _default_actor() -> ActorContext:
|
|
72
|
+
"""Zero-value ActorContext for the internal Python-API path.
|
|
73
|
+
|
|
74
|
+
Used as the default in OperationRequest when a caller does not supply an
|
|
75
|
+
explicit actor. The principal_id "local-operator" signals an in-process
|
|
76
|
+
call but is NOT authenticated (no session token). Callers that need a
|
|
77
|
+
fully-authenticated context must supply one explicitly.
|
|
78
|
+
"""
|
|
79
|
+
return ActorContext(
|
|
80
|
+
principal_id="local-operator",
|
|
81
|
+
roles=frozenset({ActorRole.OWNER}),
|
|
82
|
+
transport=Transport.INTERNAL,
|
|
83
|
+
client_host="",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class OperationRequest:
|
|
89
|
+
"""Immutable admission envelope for every mutation entering the system.
|
|
90
|
+
|
|
91
|
+
Construction rules (NON-NEGOTIABLE)
|
|
92
|
+
------------------------------------
|
|
93
|
+
1. ``actor`` is ALWAYS server-derived — session store, RBAC, daemon
|
|
94
|
+
descriptor. NEVER populated from the HTTP/MCP request body.
|
|
95
|
+
2. ``operation_id`` is auto-generated when omitted (UUID hex).
|
|
96
|
+
3. ``idempotency_key`` falls back to ``operation_id`` in __post_init__.
|
|
97
|
+
4. ``payload_hash`` is the SHA-256 of the raw content before any scrubbing;
|
|
98
|
+
callers may leave it empty for lightweight envelopes.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
operation_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
102
|
+
idempotency_key: str = ""
|
|
103
|
+
kind: OperationKind = OperationKind.REMEMBER
|
|
104
|
+
actor: ActorContext = field(default_factory=_default_actor)
|
|
105
|
+
profile_id: str = ""
|
|
106
|
+
profile_generation: int = 0
|
|
107
|
+
resource_ids: tuple[str, ...] = ()
|
|
108
|
+
scope: str = "personal"
|
|
109
|
+
deadline_ms: int = 30_000
|
|
110
|
+
schema_capability: str = ""
|
|
111
|
+
payload_hash: str = ""
|
|
112
|
+
trace_id: str = field(default_factory=lambda: uuid.uuid4().hex[:16])
|
|
113
|
+
created_at: str = field(
|
|
114
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def __post_init__(self) -> None:
|
|
118
|
+
# Guarantee: idempotency_key is never empty after construction.
|
|
119
|
+
if not self.idempotency_key:
|
|
120
|
+
object.__setattr__(self, "idempotency_key", self.operation_id)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"OperationKind",
|
|
125
|
+
"OperationRequest",
|
|
126
|
+
"OperationStatus",
|
|
127
|
+
]
|