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,270 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later — see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""SkillActivator — atomic activation of quarantined evolved skills.
|
|
6
|
+
|
|
7
|
+
Copies a VERIFIED_QUARANTINED skill artifact into the live skill directory
|
|
8
|
+
(~/.claude/skills/{skill_name}/SKILL.md), retains the prior artifact as a
|
|
9
|
+
.bak file, and provides a tested rollback path.
|
|
10
|
+
|
|
11
|
+
Invariants:
|
|
12
|
+
1. The destination directory is a child of live_root — path traversal raises ValueError.
|
|
13
|
+
2. The backup is written BEFORE the live copy is overwritten.
|
|
14
|
+
3. Activation is atomic via a .tmp file → os.replace (POSIX atomic on same fs).
|
|
15
|
+
4. Rollback restores from the .bak file and leaves the live copy unchanged.
|
|
16
|
+
5. Only the quarantined artifact identified by quarantine_dir_name is copied;
|
|
17
|
+
no arbitrary file can be activated.
|
|
18
|
+
|
|
19
|
+
CRIT-2 note: skill_name and quarantine_dir_name are ALWAYS different strings:
|
|
20
|
+
- skill_name: the original skill identifier (e.g. "brainstorming")
|
|
21
|
+
- quarantine_dir_name: the sanitized quarantine subdir (e.g. "brainstorming-vabc12")
|
|
22
|
+
Both are required; neither defaults from the other.
|
|
23
|
+
|
|
24
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import shutil
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
LIVE_SKILLS_ROOT: Path = Path.home() / ".claude" / "skills"
|
|
42
|
+
BACKUP_ROOT: Path = canonical_data_root() / "skill_backups"
|
|
43
|
+
QUARANTINE_ROOT: Path = canonical_data_root() / "quarantine" / "skills"
|
|
44
|
+
|
|
45
|
+
_SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_-]")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SkillActivationError(Exception):
|
|
49
|
+
"""Raised when activation fails after writing the backup.
|
|
50
|
+
|
|
51
|
+
The caller must invoke rollback() after receiving this error to restore
|
|
52
|
+
the previous live artifact.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SkillActivator:
|
|
57
|
+
"""Activates a quarantined evolved skill into the live skill directory."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
live_root: Path | None = None,
|
|
63
|
+
backup_root: Path | None = None,
|
|
64
|
+
quarantine_root: Path | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
self._live_root = live_root or LIVE_SKILLS_ROOT
|
|
67
|
+
self._backup_root = backup_root or BACKUP_ROOT
|
|
68
|
+
self._quarantine_root = quarantine_root or QUARANTINE_ROOT
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# Path helpers
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def _safe_skill_dir(self, skill_name: str, base: Path) -> Path:
|
|
75
|
+
"""Return base / sanitized_name, raising ValueError on traversal.
|
|
76
|
+
|
|
77
|
+
Two-layer check (fail-fast + belt-and-suspenders):
|
|
78
|
+
1. Reject the raw skill_name if it contains '..' or is absolute.
|
|
79
|
+
2. After sanitization, resolve the target path and verify it remains
|
|
80
|
+
inside base. This catches any edge case the regex misses.
|
|
81
|
+
"""
|
|
82
|
+
# Fail-fast: detect traversal in raw name before sanitization.
|
|
83
|
+
raw = Path(skill_name)
|
|
84
|
+
if raw.is_absolute() or ".." in raw.parts:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"skill_name {skill_name!r} contains path traversal sequences"
|
|
87
|
+
)
|
|
88
|
+
safe = _SAFE_NAME_RE.sub("-", skill_name).lower()[:50] or "skill"
|
|
89
|
+
target = (base / safe).resolve()
|
|
90
|
+
base_resolved = base.resolve()
|
|
91
|
+
# Audit P2-5: is_relative_to is not prefix-fragile (a sibling like
|
|
92
|
+
# ".../skills-evil" cannot masquerade as being under ".../skills").
|
|
93
|
+
if not target.is_relative_to(base_resolved):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"skill_name {skill_name!r} escapes sandbox: {target}"
|
|
96
|
+
)
|
|
97
|
+
return target
|
|
98
|
+
|
|
99
|
+
def _quarantine_path(self, quarantine_dir_name: str) -> Path:
|
|
100
|
+
"""Return the SKILL.md path inside quarantine for a given dir name.
|
|
101
|
+
|
|
102
|
+
quarantine_dir_name is the sanitized directory name (CRIT-2), e.g.
|
|
103
|
+
'brainstorming-vabc12'. It is NOT the skill_name.
|
|
104
|
+
|
|
105
|
+
Hardened (audit P0-1 / P1-4): reject absolute paths and '..' traversal,
|
|
106
|
+
then resolve() and require containment inside the quarantine root. The
|
|
107
|
+
resolve() step also defeats a symlinked quarantine dir that would
|
|
108
|
+
otherwise redirect the read outside the sandbox and inject arbitrary
|
|
109
|
+
content into a live skill.
|
|
110
|
+
"""
|
|
111
|
+
raw = Path(quarantine_dir_name)
|
|
112
|
+
if raw.is_absolute() or ".." in raw.parts:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"quarantine_dir_name {quarantine_dir_name!r} contains path traversal"
|
|
115
|
+
)
|
|
116
|
+
root = self._quarantine_root.resolve()
|
|
117
|
+
q_dir = (self._quarantine_root / quarantine_dir_name).resolve()
|
|
118
|
+
if not q_dir.is_relative_to(root):
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"quarantine_dir_name {quarantine_dir_name!r} escapes sandbox: {q_dir}"
|
|
121
|
+
)
|
|
122
|
+
q_path = (q_dir / "SKILL.md").resolve()
|
|
123
|
+
if not q_path.is_relative_to(root):
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"quarantine artifact escapes sandbox: {q_path}"
|
|
126
|
+
)
|
|
127
|
+
return q_path
|
|
128
|
+
|
|
129
|
+
def _live_path(self, skill_name: str) -> Path:
|
|
130
|
+
return self._safe_skill_dir(skill_name, self._live_root) / "SKILL.md"
|
|
131
|
+
|
|
132
|
+
def _backup_path(self, skill_name: str) -> Path:
|
|
133
|
+
return self._safe_skill_dir(skill_name, self._backup_root) / "SKILL.md.bak"
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# Public API
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def activate(
|
|
140
|
+
self,
|
|
141
|
+
skill_name: str,
|
|
142
|
+
quarantine_dir_name: str,
|
|
143
|
+
*,
|
|
144
|
+
actor_id: str = "",
|
|
145
|
+
) -> dict:
|
|
146
|
+
"""Atomically activate a quarantined skill.
|
|
147
|
+
|
|
148
|
+
Steps:
|
|
149
|
+
1. Resolve quarantine_path using quarantine_dir_name and verify it exists.
|
|
150
|
+
2. Read quarantine content and compute SHA-256.
|
|
151
|
+
3. Write backup of existing live SKILL.md (if any) to backup_root.
|
|
152
|
+
4. Write quarantine content to live path via .tmp → os.replace.
|
|
153
|
+
5. Return activation metadata.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
skill_name: The original skill identifier (CRIT-2), e.g. "brainstorming".
|
|
157
|
+
quarantine_dir_name: The sanitized quarantine subdir, e.g. "brainstorming-vabc12".
|
|
158
|
+
actor_id: Who triggered activation (e.g. "auto" or a user id).
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
FileNotFoundError: Quarantine artifact does not exist.
|
|
162
|
+
ValueError: Path traversal detected in skill_name.
|
|
163
|
+
SkillActivationError: Any OS error after the backup was already written.
|
|
164
|
+
"""
|
|
165
|
+
q_path = self._quarantine_path(quarantine_dir_name)
|
|
166
|
+
live_path = self._live_path(skill_name) # raises ValueError on traversal
|
|
167
|
+
backup_path = self._backup_path(skill_name)
|
|
168
|
+
|
|
169
|
+
if not q_path.exists():
|
|
170
|
+
raise FileNotFoundError(
|
|
171
|
+
f"Quarantine artifact not found: {q_path}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
content = q_path.read_bytes()
|
|
175
|
+
content_hash = hashlib.sha256(content).hexdigest()
|
|
176
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
177
|
+
|
|
178
|
+
# Step 3: backup existing live artifact BEFORE any overwrite
|
|
179
|
+
backup_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
backup_written = False
|
|
181
|
+
if live_path.exists():
|
|
182
|
+
shutil.copy2(live_path, backup_path)
|
|
183
|
+
backup_written = True
|
|
184
|
+
logger.info(
|
|
185
|
+
"skill_activator: backed up %s → %s",
|
|
186
|
+
live_path, backup_path,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Step 4: atomic write via .tmp → rename (same directory = same fs)
|
|
190
|
+
live_path.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
tmp_path = live_path.with_suffix(".tmp")
|
|
192
|
+
try:
|
|
193
|
+
tmp_path.write_bytes(content)
|
|
194
|
+
os.replace(tmp_path, live_path)
|
|
195
|
+
except OSError as exc:
|
|
196
|
+
try:
|
|
197
|
+
tmp_path.unlink(missing_ok=True)
|
|
198
|
+
except OSError:
|
|
199
|
+
pass
|
|
200
|
+
raise SkillActivationError(
|
|
201
|
+
f"atomic write failed for {live_path}: {exc}"
|
|
202
|
+
) from exc
|
|
203
|
+
|
|
204
|
+
logger.info(
|
|
205
|
+
"skill_activator: activated %s @ %s (sha256=%s, actor=%s)",
|
|
206
|
+
skill_name, live_path, content_hash[:12], actor_id,
|
|
207
|
+
)
|
|
208
|
+
return {
|
|
209
|
+
"skill_name": skill_name,
|
|
210
|
+
"live_path": str(live_path),
|
|
211
|
+
"backup_path": str(backup_path) if backup_written else None,
|
|
212
|
+
"content_hash": content_hash,
|
|
213
|
+
"activated_at": ts,
|
|
214
|
+
"actor_id": actor_id,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def rollback(self, skill_name: str) -> dict:
|
|
218
|
+
"""Restore the live SKILL.md from the backup written by activate().
|
|
219
|
+
|
|
220
|
+
If no backup exists, removes the live file (the skill was new).
|
|
221
|
+
Never raises on missing backup — logs a warning instead.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
skill_name: The original skill identifier, same value used in activate().
|
|
225
|
+
"""
|
|
226
|
+
live_path = self._live_path(skill_name)
|
|
227
|
+
backup_path = self._backup_path(skill_name)
|
|
228
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
229
|
+
|
|
230
|
+
if backup_path.exists():
|
|
231
|
+
# Audit P2-4: restore atomically (tmp → os.replace) so a crash
|
|
232
|
+
# mid-rollback cannot leave a torn live SKILL.md.
|
|
233
|
+
live_path.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
tmp_path = live_path.with_suffix(".tmp")
|
|
235
|
+
tmp_path.write_bytes(backup_path.read_bytes())
|
|
236
|
+
os.replace(tmp_path, live_path)
|
|
237
|
+
logger.info(
|
|
238
|
+
"skill_activator: rolled back %s ← %s",
|
|
239
|
+
live_path, backup_path,
|
|
240
|
+
)
|
|
241
|
+
return {
|
|
242
|
+
"skill_name": skill_name,
|
|
243
|
+
"rolled_back": True,
|
|
244
|
+
"restored_from": str(backup_path),
|
|
245
|
+
"rolled_back_at": ts,
|
|
246
|
+
}
|
|
247
|
+
elif live_path.exists():
|
|
248
|
+
live_path.unlink()
|
|
249
|
+
logger.warning(
|
|
250
|
+
"skill_activator: no backup for %s; removed live file",
|
|
251
|
+
skill_name,
|
|
252
|
+
)
|
|
253
|
+
return {
|
|
254
|
+
"skill_name": skill_name,
|
|
255
|
+
"rolled_back": True,
|
|
256
|
+
"restored_from": None,
|
|
257
|
+
"note": "no backup; live file removed",
|
|
258
|
+
"rolled_back_at": ts,
|
|
259
|
+
}
|
|
260
|
+
else:
|
|
261
|
+
logger.warning(
|
|
262
|
+
"skill_activator: rollback called but no live file for %s",
|
|
263
|
+
skill_name,
|
|
264
|
+
)
|
|
265
|
+
return {
|
|
266
|
+
"skill_name": skill_name,
|
|
267
|
+
"rolled_back": False,
|
|
268
|
+
"note": "nothing to rollback",
|
|
269
|
+
"rolled_back_at": ts,
|
|
270
|
+
}
|