superlocalmemory 3.7.7 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- 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 +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +21 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -32,6 +32,7 @@ logger = logging.getLogger(__name__)
|
|
|
32
32
|
_SCHEMA_DDL = """
|
|
33
33
|
CREATE TABLE IF NOT EXISTS skill_evolution_log (
|
|
34
34
|
id TEXT PRIMARY KEY,
|
|
35
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
35
36
|
skill_name TEXT NOT NULL,
|
|
36
37
|
parent_skill_id TEXT,
|
|
37
38
|
evolution_type TEXT NOT NULL,
|
|
@@ -49,14 +50,16 @@ CREATE TABLE IF NOT EXISTS skill_evolution_log (
|
|
|
49
50
|
completed_at TEXT
|
|
50
51
|
);
|
|
51
52
|
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_evo_skill ON skill_evolution_log(skill_name);
|
|
53
|
-
CREATE INDEX IF NOT EXISTS idx_evo_status ON skill_evolution_log(status);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_evo_created ON skill_evolution_log(created_at);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_evo_skill ON skill_evolution_log(profile_id, skill_name);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_evo_status ON skill_evolution_log(profile_id, status);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_evo_created ON skill_evolution_log(profile_id, created_at);
|
|
55
56
|
|
|
56
57
|
CREATE TABLE IF NOT EXISTS evolution_cycle_state (
|
|
57
|
-
|
|
58
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
59
|
+
key TEXT NOT NULL,
|
|
58
60
|
value INTEGER DEFAULT 0,
|
|
59
|
-
updated_at TEXT
|
|
61
|
+
updated_at TEXT,
|
|
62
|
+
PRIMARY KEY (profile_id, key)
|
|
60
63
|
);
|
|
61
64
|
"""
|
|
62
65
|
|
|
@@ -77,6 +80,11 @@ class EvolutionStore:
|
|
|
77
80
|
def _ensure_schema(self) -> None:
|
|
78
81
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
79
82
|
try:
|
|
83
|
+
# Migrate an existing pre-isolation DB BEFORE running the schema DDL:
|
|
84
|
+
# the DDL creates indexes on profile_id, which would fail on a legacy
|
|
85
|
+
# table that still lacks the column. On a fresh DB the migration is a
|
|
86
|
+
# no-op (tables absent) and the DDL creates everything correctly.
|
|
87
|
+
self._migrate_profile_isolation(conn)
|
|
80
88
|
conn.executescript(_SCHEMA_DDL)
|
|
81
89
|
conn.commit()
|
|
82
90
|
except sqlite3.OperationalError as exc:
|
|
@@ -84,56 +92,113 @@ class EvolutionStore:
|
|
|
84
92
|
finally:
|
|
85
93
|
conn.close()
|
|
86
94
|
|
|
87
|
-
def
|
|
88
|
-
"""
|
|
95
|
+
def _migrate_profile_isolation(self, conn: sqlite3.Connection) -> None:
|
|
96
|
+
"""Self-migrate pre-profile-isolation DBs to add per-profile scoping.
|
|
97
|
+
|
|
98
|
+
These tables are store-owned (created lazily here, not by the schema
|
|
99
|
+
migration runner which fires before this store exists), so the store
|
|
100
|
+
owns their upgrade too. Idempotent: only alters when the column/PK is
|
|
101
|
+
missing. Existing rows backfill to 'default' — the historical profile.
|
|
102
|
+
"""
|
|
103
|
+
# skill_evolution_log: additive column (safe, preserves rows).
|
|
104
|
+
cols = {r[1] for r in conn.execute(
|
|
105
|
+
"PRAGMA table_info(skill_evolution_log)").fetchall()}
|
|
106
|
+
if cols and "profile_id" not in cols:
|
|
107
|
+
conn.execute(
|
|
108
|
+
"ALTER TABLE skill_evolution_log "
|
|
109
|
+
"ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default'"
|
|
110
|
+
)
|
|
111
|
+
conn.execute(
|
|
112
|
+
"CREATE INDEX IF NOT EXISTS idx_evo_skill "
|
|
113
|
+
"ON skill_evolution_log(profile_id, skill_name)"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# evolution_cycle_state: PK changes from (key) to (profile_id, key).
|
|
117
|
+
# SQLite cannot alter a PK in place → rebuild + copy, backfilling
|
|
118
|
+
# existing counters to the 'default' profile.
|
|
119
|
+
cyc_cols = {r[1] for r in conn.execute(
|
|
120
|
+
"PRAGMA table_info(evolution_cycle_state)").fetchall()}
|
|
121
|
+
if cyc_cols and "profile_id" not in cyc_cols:
|
|
122
|
+
conn.execute(
|
|
123
|
+
"ALTER TABLE evolution_cycle_state RENAME TO _evo_cycle_old"
|
|
124
|
+
)
|
|
125
|
+
conn.execute(
|
|
126
|
+
"CREATE TABLE evolution_cycle_state ("
|
|
127
|
+
" profile_id TEXT NOT NULL DEFAULT 'default',"
|
|
128
|
+
" key TEXT NOT NULL,"
|
|
129
|
+
" value INTEGER DEFAULT 0,"
|
|
130
|
+
" updated_at TEXT,"
|
|
131
|
+
" PRIMARY KEY (profile_id, key))"
|
|
132
|
+
)
|
|
133
|
+
conn.execute(
|
|
134
|
+
"INSERT INTO evolution_cycle_state "
|
|
135
|
+
"(profile_id, key, value, updated_at) "
|
|
136
|
+
"SELECT 'default', key, value, updated_at FROM _evo_cycle_old"
|
|
137
|
+
)
|
|
138
|
+
conn.execute("DROP TABLE _evo_cycle_old")
|
|
139
|
+
|
|
140
|
+
def reset_cycle(self, profile_id: str) -> None:
|
|
141
|
+
"""Reset per-cycle counters for one profile. Called at cycle start.
|
|
142
|
+
|
|
143
|
+
The evolve budget is per-profile: one profile exhausting its budget
|
|
144
|
+
must never block another profile from evolving.
|
|
145
|
+
"""
|
|
89
146
|
now = datetime.now(timezone.utc).isoformat()
|
|
90
147
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
91
148
|
try:
|
|
92
149
|
conn.execute(
|
|
93
|
-
"INSERT OR REPLACE INTO evolution_cycle_state
|
|
94
|
-
"
|
|
95
|
-
(
|
|
150
|
+
"INSERT OR REPLACE INTO evolution_cycle_state "
|
|
151
|
+
"(profile_id, key, value, updated_at) "
|
|
152
|
+
"VALUES (?, 'cycle_count', 0, ?)",
|
|
153
|
+
(profile_id, now),
|
|
96
154
|
)
|
|
97
155
|
conn.commit()
|
|
98
156
|
finally:
|
|
99
157
|
conn.close()
|
|
100
158
|
|
|
101
|
-
def can_evolve(self) -> bool:
|
|
102
|
-
"""Check if budget allows another evolution this cycle."""
|
|
159
|
+
def can_evolve(self, profile_id: str) -> bool:
|
|
160
|
+
"""Check if this profile's budget allows another evolution this cycle."""
|
|
103
161
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
104
162
|
try:
|
|
105
163
|
row = conn.execute(
|
|
106
|
-
"SELECT value FROM evolution_cycle_state
|
|
164
|
+
"SELECT value FROM evolution_cycle_state "
|
|
165
|
+
"WHERE profile_id = ? AND key = 'cycle_count'",
|
|
166
|
+
(profile_id,),
|
|
107
167
|
).fetchone()
|
|
108
168
|
count = row[0] if row else 0
|
|
109
169
|
return count < MAX_EVOLUTIONS_PER_CYCLE
|
|
110
170
|
finally:
|
|
111
171
|
conn.close()
|
|
112
172
|
|
|
113
|
-
def record_evolution_attempt(self) -> None:
|
|
114
|
-
"""Increment cycle counter in DB."""
|
|
173
|
+
def record_evolution_attempt(self, profile_id: str) -> None:
|
|
174
|
+
"""Increment this profile's cycle counter in DB."""
|
|
115
175
|
now = datetime.now(timezone.utc).isoformat()
|
|
116
176
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
117
177
|
try:
|
|
118
178
|
row = conn.execute(
|
|
119
|
-
"SELECT value FROM evolution_cycle_state
|
|
179
|
+
"SELECT value FROM evolution_cycle_state "
|
|
180
|
+
"WHERE profile_id = ? AND key = 'cycle_count'",
|
|
181
|
+
(profile_id,),
|
|
120
182
|
).fetchone()
|
|
121
183
|
current = row[0] if row else 0
|
|
122
184
|
conn.execute(
|
|
123
|
-
"INSERT OR REPLACE INTO evolution_cycle_state
|
|
124
|
-
"
|
|
125
|
-
(
|
|
185
|
+
"INSERT OR REPLACE INTO evolution_cycle_state "
|
|
186
|
+
"(profile_id, key, value, updated_at) "
|
|
187
|
+
"VALUES (?, 'cycle_count', ?, ?)",
|
|
188
|
+
(profile_id, current + 1, now),
|
|
126
189
|
)
|
|
127
190
|
conn.commit()
|
|
128
191
|
finally:
|
|
129
192
|
conn.close()
|
|
130
193
|
|
|
131
|
-
def _get_cycle_count(self) -> int:
|
|
132
|
-
"""Read current cycle count from DB."""
|
|
194
|
+
def _get_cycle_count(self, profile_id: str) -> int:
|
|
195
|
+
"""Read this profile's current cycle count from DB."""
|
|
133
196
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
134
197
|
try:
|
|
135
198
|
row = conn.execute(
|
|
136
|
-
"SELECT value FROM evolution_cycle_state
|
|
199
|
+
"SELECT value FROM evolution_cycle_state "
|
|
200
|
+
"WHERE profile_id = ? AND key = 'cycle_count'",
|
|
201
|
+
(profile_id,),
|
|
137
202
|
).fetchone()
|
|
138
203
|
return row[0] if row else 0
|
|
139
204
|
finally:
|
|
@@ -162,18 +227,19 @@ class EvolutionStore:
|
|
|
162
227
|
# CRUD
|
|
163
228
|
# ------------------------------------------------------------------
|
|
164
229
|
|
|
165
|
-
def save_record(self, record: EvolutionRecord) -> None:
|
|
230
|
+
def save_record(self, record: EvolutionRecord, profile_id: str) -> None:
|
|
166
231
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
167
232
|
try:
|
|
168
233
|
conn.execute(
|
|
169
234
|
"INSERT OR REPLACE INTO skill_evolution_log "
|
|
170
|
-
"(id, skill_name, parent_skill_id, evolution_type,
|
|
171
|
-
" generation, status, mutation_summary, evidence, "
|
|
235
|
+
"(id, profile_id, skill_name, parent_skill_id, evolution_type, "
|
|
236
|
+
" trigger_type, generation, status, mutation_summary, evidence, "
|
|
172
237
|
" original_content, evolved_content, content_diff, "
|
|
173
238
|
" blind_verified, rejection_reason, created_at, completed_at) "
|
|
174
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
239
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
175
240
|
(
|
|
176
241
|
record.id,
|
|
242
|
+
profile_id,
|
|
177
243
|
record.skill_name,
|
|
178
244
|
record.parent_skill_id,
|
|
179
245
|
record.evolution_type.value,
|
|
@@ -195,13 +261,14 @@ class EvolutionStore:
|
|
|
195
261
|
finally:
|
|
196
262
|
conn.close()
|
|
197
263
|
|
|
198
|
-
def get_record(self, record_id: str) -> Optional[EvolutionRecord]:
|
|
264
|
+
def get_record(self, record_id: str, profile_id: str) -> Optional[EvolutionRecord]:
|
|
199
265
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
200
266
|
conn.row_factory = sqlite3.Row
|
|
201
267
|
try:
|
|
202
268
|
row = conn.execute(
|
|
203
|
-
"SELECT * FROM skill_evolution_log
|
|
204
|
-
|
|
269
|
+
"SELECT * FROM skill_evolution_log "
|
|
270
|
+
"WHERE id = ? AND profile_id = ?",
|
|
271
|
+
(record_id, profile_id),
|
|
205
272
|
).fetchone()
|
|
206
273
|
if not row:
|
|
207
274
|
return None
|
|
@@ -209,68 +276,78 @@ class EvolutionStore:
|
|
|
209
276
|
finally:
|
|
210
277
|
conn.close()
|
|
211
278
|
|
|
212
|
-
def get_skill_history(
|
|
279
|
+
def get_skill_history(
|
|
280
|
+
self, skill_name: str, profile_id: str, limit: int = 20,
|
|
281
|
+
) -> list[EvolutionRecord]:
|
|
213
282
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
214
283
|
conn.row_factory = sqlite3.Row
|
|
215
284
|
try:
|
|
216
285
|
rows = conn.execute(
|
|
217
286
|
"SELECT * FROM skill_evolution_log "
|
|
218
|
-
"WHERE skill_name = ?
|
|
219
|
-
|
|
287
|
+
"WHERE skill_name = ? AND profile_id = ? "
|
|
288
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
289
|
+
(skill_name, profile_id, limit),
|
|
220
290
|
).fetchall()
|
|
221
291
|
return [self._row_to_record(dict(r)) for r in rows]
|
|
222
292
|
finally:
|
|
223
293
|
conn.close()
|
|
224
294
|
|
|
225
|
-
def get_recent(self, limit: int = 10) -> list[EvolutionRecord]:
|
|
295
|
+
def get_recent(self, profile_id: str, limit: int = 10) -> list[EvolutionRecord]:
|
|
226
296
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
227
297
|
conn.row_factory = sqlite3.Row
|
|
228
298
|
try:
|
|
229
299
|
rows = conn.execute(
|
|
230
300
|
"SELECT * FROM skill_evolution_log "
|
|
231
|
-
"ORDER BY created_at DESC LIMIT ?",
|
|
232
|
-
(limit
|
|
301
|
+
"WHERE profile_id = ? ORDER BY created_at DESC LIMIT ?",
|
|
302
|
+
(profile_id, limit),
|
|
233
303
|
).fetchall()
|
|
234
304
|
return [self._row_to_record(dict(r)) for r in rows]
|
|
235
305
|
finally:
|
|
236
306
|
conn.close()
|
|
237
307
|
|
|
238
|
-
def count_attempts(self, skill_name: str) -> int:
|
|
308
|
+
def count_attempts(self, skill_name: str, profile_id: str) -> int:
|
|
239
309
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
240
310
|
try:
|
|
241
311
|
row = conn.execute(
|
|
242
312
|
"SELECT COUNT(*) FROM skill_evolution_log "
|
|
243
|
-
"WHERE skill_name = ? AND
|
|
244
|
-
(
|
|
313
|
+
"WHERE skill_name = ? AND profile_id = ? "
|
|
314
|
+
"AND status NOT IN ('promoted')",
|
|
315
|
+
(skill_name, profile_id),
|
|
245
316
|
).fetchone()
|
|
246
317
|
return row[0] if row else 0
|
|
247
318
|
finally:
|
|
248
319
|
conn.close()
|
|
249
320
|
|
|
250
|
-
def has_exceeded_attempts(self, skill_name: str) -> bool:
|
|
251
|
-
return self.count_attempts(skill_name) >= MAX_ATTEMPTS_PER_SKILL
|
|
321
|
+
def has_exceeded_attempts(self, skill_name: str, profile_id: str) -> bool:
|
|
322
|
+
return self.count_attempts(skill_name, profile_id) >= MAX_ATTEMPTS_PER_SKILL
|
|
252
323
|
|
|
253
|
-
def get_stats(self) -> dict:
|
|
324
|
+
def get_stats(self, profile_id: str) -> dict:
|
|
254
325
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
255
326
|
try:
|
|
256
327
|
total = conn.execute(
|
|
257
|
-
"SELECT COUNT(*) FROM skill_evolution_log",
|
|
328
|
+
"SELECT COUNT(*) FROM skill_evolution_log WHERE profile_id = ?",
|
|
329
|
+
(profile_id,),
|
|
258
330
|
).fetchone()[0]
|
|
259
331
|
by_status = {}
|
|
260
332
|
for row in conn.execute(
|
|
261
|
-
"SELECT status, COUNT(*) FROM skill_evolution_log
|
|
333
|
+
"SELECT status, COUNT(*) FROM skill_evolution_log "
|
|
334
|
+
"WHERE profile_id = ? GROUP BY status",
|
|
335
|
+
(profile_id,),
|
|
262
336
|
).fetchall():
|
|
263
337
|
by_status[row[0]] = row[1]
|
|
264
338
|
by_type = {}
|
|
265
339
|
for row in conn.execute(
|
|
266
|
-
"SELECT evolution_type, COUNT(*) FROM skill_evolution_log
|
|
340
|
+
"SELECT evolution_type, COUNT(*) FROM skill_evolution_log "
|
|
341
|
+
"WHERE profile_id = ? GROUP BY evolution_type",
|
|
342
|
+
(profile_id,),
|
|
267
343
|
).fetchall():
|
|
268
344
|
by_type[row[0]] = row[1]
|
|
269
345
|
return {
|
|
270
346
|
"total": total,
|
|
271
347
|
"by_status": by_status,
|
|
272
348
|
"by_type": by_type,
|
|
273
|
-
"cycle_budget_remaining":
|
|
349
|
+
"cycle_budget_remaining":
|
|
350
|
+
MAX_EVOLUTIONS_PER_CYCLE - self._get_cycle_count(profile_id),
|
|
274
351
|
}
|
|
275
352
|
finally:
|
|
276
353
|
conn.close()
|
|
@@ -162,7 +162,18 @@ ALLOWED_LLM_MODELS: frozenset[str] = frozenset({
|
|
|
162
162
|
|
|
163
163
|
FORBIDDEN_MODEL_SUBSTRINGS: tuple[str, ...] = ("opus", "gpt-4-turbo")
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
# Hard ceiling on ``max_tokens`` for any single evolution LLM call
|
|
166
|
+
# (LLD-11). This is a CEILING, not a per-call default: each caller passes
|
|
167
|
+
# its own budget (confirm=100, blind-verify=500, mutation=4000). The value
|
|
168
|
+
# MUST stay >= the largest legitimate caller request — otherwise that call
|
|
169
|
+
# trips the guard in ``_dispatch_llm`` and silently fails-closed. The
|
|
170
|
+
# mutation step (``SkillEvolver._generate_mutation``) generates a full
|
|
171
|
+
# SKILL.md and requests 4000; a stale 500 here made mutation return "" on
|
|
172
|
+
# every cycle, killing evolution invisibly. 8000 accommodates 4000 with
|
|
173
|
+
# retry/large-skill headroom. Do NOT lower below 4096 without first
|
|
174
|
+
# shrinking that caller's request. Per-call spend is still bounded by
|
|
175
|
+
# EvolutionBudget (10 LLM calls/cycle x 3 cycles/day).
|
|
176
|
+
MAX_TOKENS_CAP: int = 8000
|
|
166
177
|
|
|
167
178
|
|
|
168
179
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory v3.7.9 — evolution model selection
|
|
4
|
+
|
|
5
|
+
"""Per-step LLM model selection for the skill-evolution pipeline.
|
|
6
|
+
|
|
7
|
+
v3.7.9: skill evolution defaults to the *lowest-cost* capable model for
|
|
8
|
+
whatever backend is active, and lets users override each step
|
|
9
|
+
(``evolution.mutation_model`` / ``verify_model`` / ``confirm_model``).
|
|
10
|
+
|
|
11
|
+
Two invariants this module enforces:
|
|
12
|
+
|
|
13
|
+
1. **Lowest cost by default.** Claude backend → Haiku; Ollama → a local
|
|
14
|
+
(free) model. No step silently defaults to a premium model.
|
|
15
|
+
2. **Generator != verifier.** Blind verification must run on a *different*
|
|
16
|
+
model from the generator, otherwise the generator effectively grades
|
|
17
|
+
its own homework. When an independent verifier isn't available for
|
|
18
|
+
free (Claude backend, no local Ollama), we reuse the cheapest model
|
|
19
|
+
and set ``independent=False`` so the caller can log the reduced-safety
|
|
20
|
+
condition — we never escalate the verifier to a premium model just to
|
|
21
|
+
force independence (that would defeat the cost goal).
|
|
22
|
+
|
|
23
|
+
This module is pure: it takes ``ollama_available`` as an argument instead
|
|
24
|
+
of probing, so it is fully unit-testable without network access.
|
|
25
|
+
|
|
26
|
+
Author: Varun Pratap Bhardwaj / Qualixar
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Allow-listed model ids (must stay a subset of
|
|
37
|
+
# ``llm_dispatch.ALLOWED_LLM_MODELS``).
|
|
38
|
+
CHEAPEST_CLAUDE = "claude-haiku-4-5"
|
|
39
|
+
QUALITY_CLAUDE = "claude-sonnet-4-6"
|
|
40
|
+
CHEAPEST_OLLAMA = "ollama:llama3"
|
|
41
|
+
ALT_OLLAMA = "ollama:qwen2.5"
|
|
42
|
+
|
|
43
|
+
# Short-name → allow-listed model id. Single source of truth for aliasing;
|
|
44
|
+
# ``skill_evolver`` re-exports these for backward compatibility.
|
|
45
|
+
_MODEL_ALIASES: dict[str, str] = {
|
|
46
|
+
"haiku": CHEAPEST_CLAUDE,
|
|
47
|
+
"sonnet": QUALITY_CLAUDE,
|
|
48
|
+
"ollama": CHEAPEST_OLLAMA,
|
|
49
|
+
"ollama:llama3": CHEAPEST_OLLAMA,
|
|
50
|
+
"ollama:qwen2.5": ALT_OLLAMA,
|
|
51
|
+
CHEAPEST_CLAUDE: CHEAPEST_CLAUDE,
|
|
52
|
+
QUALITY_CLAUDE: QUALITY_CLAUDE,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Models this subsystem is allowed to dispatch. Anything resolving outside
|
|
56
|
+
# this set falls back to the cheapest safe default rather than dispatching
|
|
57
|
+
# an unvetted (or forbidden) model.
|
|
58
|
+
_ALLOWED: frozenset[str] = frozenset(_MODEL_ALIASES.values())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resolve_model_alias(alias: str) -> str:
|
|
62
|
+
"""Translate a short alias (``haiku``/``sonnet``) to an allow-listed id.
|
|
63
|
+
|
|
64
|
+
Unknown aliases fall back to the cheapest Claude model — the same
|
|
65
|
+
fail-safe the evolver has always used.
|
|
66
|
+
"""
|
|
67
|
+
return _MODEL_ALIASES.get(alias, CHEAPEST_CLAUDE)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class ResolvedModels:
|
|
72
|
+
"""Concrete allow-listed model id for each pipeline step."""
|
|
73
|
+
|
|
74
|
+
mutation: str
|
|
75
|
+
verify: str
|
|
76
|
+
confirm: str
|
|
77
|
+
independent: bool # True when verify != mutation (blind-verify is sound)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cheapest_for_backend(backend: str) -> str:
|
|
81
|
+
"""Lowest-cost allow-listed model for a detected backend."""
|
|
82
|
+
if backend == "ollama":
|
|
83
|
+
return CHEAPEST_OLLAMA
|
|
84
|
+
# claude CLI, anthropic API, and any non-Ollama backend evolution can
|
|
85
|
+
# actually dispatch route through the cheapest Claude model.
|
|
86
|
+
return CHEAPEST_CLAUDE
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resolve_field(value: str, default: str) -> str:
|
|
90
|
+
"""Resolve a config field: empty → default; else alias→id, validated."""
|
|
91
|
+
if not value:
|
|
92
|
+
return default
|
|
93
|
+
resolved = _resolve_model_alias(value)
|
|
94
|
+
if resolved not in _ALLOWED: # pragma: no cover — defensive
|
|
95
|
+
logger.warning(
|
|
96
|
+
"evolution: configured model %r is not allow-listed; "
|
|
97
|
+
"falling back to %s", value, default,
|
|
98
|
+
)
|
|
99
|
+
return default
|
|
100
|
+
return resolved
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _independent_verifier(
|
|
104
|
+
mutation: str, *, ollama_available: bool,
|
|
105
|
+
) -> str:
|
|
106
|
+
"""Pick a cheap verifier that differs from the generator when possible.
|
|
107
|
+
|
|
108
|
+
- Ollama generator → the *other* local model (both free, distinct).
|
|
109
|
+
- Claude generator + local Ollama up → free local verifier.
|
|
110
|
+
- Claude generator, no Ollama → the cheapest *different* Claude tier if
|
|
111
|
+
the generator was a premium model; otherwise reuse the cheapest model
|
|
112
|
+
(caller flags reduced independence — we don't pay for a premium
|
|
113
|
+
verifier just to force distinctness).
|
|
114
|
+
"""
|
|
115
|
+
if mutation.startswith("ollama:"):
|
|
116
|
+
return ALT_OLLAMA if mutation != ALT_OLLAMA else CHEAPEST_OLLAMA
|
|
117
|
+
if ollama_available:
|
|
118
|
+
return CHEAPEST_OLLAMA
|
|
119
|
+
if mutation != CHEAPEST_CLAUDE:
|
|
120
|
+
return CHEAPEST_CLAUDE
|
|
121
|
+
return CHEAPEST_CLAUDE
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_evolution_models(
|
|
125
|
+
config: object, backend: str, *, ollama_available: bool,
|
|
126
|
+
) -> ResolvedModels:
|
|
127
|
+
"""Resolve mutation/verify/confirm models for a backend.
|
|
128
|
+
|
|
129
|
+
``config`` is an ``EvolutionConfig`` (duck-typed: reads
|
|
130
|
+
``mutation_model`` / ``verify_model`` / ``confirm_model`` attributes,
|
|
131
|
+
each an alias, allow-listed id, or "" for auto).
|
|
132
|
+
"""
|
|
133
|
+
cheapest = _cheapest_for_backend(backend)
|
|
134
|
+
|
|
135
|
+
mutation = _resolve_field(getattr(config, "mutation_model", ""), cheapest)
|
|
136
|
+
confirm = _resolve_field(getattr(config, "confirm_model", ""), cheapest)
|
|
137
|
+
|
|
138
|
+
verify_cfg = getattr(config, "verify_model", "")
|
|
139
|
+
if verify_cfg:
|
|
140
|
+
verify = _resolve_field(verify_cfg, cheapest)
|
|
141
|
+
else:
|
|
142
|
+
verify = _independent_verifier(
|
|
143
|
+
mutation, ollama_available=ollama_available,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
independent = verify != mutation
|
|
147
|
+
if not independent:
|
|
148
|
+
logger.warning(
|
|
149
|
+
"evolution: blind-verify model == generator model (%s) — "
|
|
150
|
+
"blind-verification independence is reduced. Set "
|
|
151
|
+
"evolution.verify_model to a different model, or run Ollama "
|
|
152
|
+
"locally, for an independent verifier.",
|
|
153
|
+
mutation,
|
|
154
|
+
)
|
|
155
|
+
return ResolvedModels(
|
|
156
|
+
mutation=mutation,
|
|
157
|
+
verify=verify,
|
|
158
|
+
confirm=confirm,
|
|
159
|
+
independent=independent,
|
|
160
|
+
)
|
|
@@ -89,6 +89,17 @@ def parse_mutation_output(output: str) -> Optional[str]:
|
|
|
89
89
|
return None
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
# An evolved SKILL.md is auto-loaded into future sessions, so a mutation that
|
|
93
|
+
# hallucinated (or was prompt-injected into producing) code-execution or
|
|
94
|
+
# secret-exfiltration instructions must never be persisted. Structure checks
|
|
95
|
+
# alone are insufficient — reject dangerous content outright.
|
|
96
|
+
_SKILL_DENY_PATTERNS: tuple[str, ...] = (
|
|
97
|
+
"os.environ", "subprocess", "exec(", "eval(", "__import__", "import os",
|
|
98
|
+
"import subprocess", "pickle.loads", "curl ", "wget ", "rm -rf",
|
|
99
|
+
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "AWS_SECRET", "/.ssh/", ".install_token",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
92
103
|
def validate_skill_content(content: str) -> Optional[str]:
|
|
93
104
|
"""Validate evolved skill content. Returns error message or None if valid."""
|
|
94
105
|
if not content or len(content) < 50:
|
|
@@ -97,6 +108,11 @@ def validate_skill_content(content: str) -> Optional[str]:
|
|
|
97
108
|
return "Missing YAML frontmatter (no --- found)"
|
|
98
109
|
if content.count("---") >= 2 and "name:" not in content.split("---")[1]:
|
|
99
110
|
return "Missing 'name:' in frontmatter"
|
|
111
|
+
lowered = content.lower()
|
|
112
|
+
for pat in _SKILL_DENY_PATTERNS:
|
|
113
|
+
if pat.lower() in lowered:
|
|
114
|
+
return (f"Rejected: evolved skill contains a disallowed pattern "
|
|
115
|
+
f"({pat!r}) — skills must not execute code or access secrets")
|
|
100
116
|
return None
|
|
101
117
|
|
|
102
118
|
|