Add semantic drift monitor with CLI and security hardening#2
Add semantic drift monitor with CLI and security hardening#2Knapp-Kevin wants to merge 2 commits intossdavidai:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional “Semantic Drift Monitor” to Surveyor to persist cluster snapshots, compare consecutive runs, and expose drift summaries via new CLI commands, along with config + documentation updates.
Changes:
- Introduce
DriftMonitorto snapshot cluster membership and write drift reports. - Integrate drift processing into the Surveyor daemon and add
alfred drift {status|history|show}CLI. - Add config schema + examples/docs for feature flag and drift settings.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/alfred/surveyor/drift.py | New snapshot + drift comparison logic, report writing, pruning, and path confinement for report reads |
| src/alfred/surveyor/drift_cli.py | New CLI handlers for drift status/history/show |
| src/alfred/surveyor/daemon.py | Integrates drift processing into the Surveyor pipeline after clustering/labeling |
| src/alfred/surveyor/config.py | Adds FeatureFlags and SemanticDriftConfig to typed config loading |
| src/alfred/cli.py | Adds top-level alfred drift command wiring and dispatch |
| src/alfred/quickstart.py | Includes drift feature/config defaults in generated config |
| src/alfred/_bundled/config.yaml.example | Documents new config keys in bundled example |
| config.yaml.example | Documents new config keys in root example |
| docs/Surveyor.md | Documents drift settings and artifacts |
| docs/CLI-Commands.md | Documents new alfred drift commands |
| INITIAL_SECURITY_REVIEW.md | Adds security review notes for drift monitor |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| all_changed = result.changed_semantic | result.changed_structural | ||
| if not all_changed: | ||
| log.info("daemon.no_changed_clusters") | ||
| return | ||
| else: |
There was a problem hiding this comment.
The control flow now logs daemon.no_changed_clusters but still continues into drift processing and then logs daemon.labeling_complete. This results in two potentially contradictory log events for the no-change case. Consider returning early after no_changed_clusters, or renaming/guarding the later log so it only fires when labeling actually ran (or rename it to reflect the full step including drift).
| cluster_key = f"semantic_{cid}" | ||
| from .state import ClusterState | ||
| from datetime import datetime, timezone | ||
| self.state.clusters[cluster_key] = ClusterState( |
There was a problem hiding this comment.
Imports inside the per-cluster loop (from .state import ClusterState, from datetime import ...) reduce readability and are executed repeatedly (even if cached). Since ClusterState and datetime/timezone are already safe to import at module scope, prefer moving these imports to the top of the file (or at least above the loop).
| All configuration lives in `config.yaml` under the `surveyor` section. | ||
|
|
||
| ### Full Example | ||
|
|
||
| ```yaml | ||
| features: | ||
| semantic_drift_monitor: false | ||
|
|
||
| semantic_drift: | ||
| snapshot_retention: 10 | ||
| similarity_threshold: 0.5 | ||
| warn_on_high_drift: true |
There was a problem hiding this comment.
The docs say “All configuration lives in config.yaml under the surveyor section.” but the example immediately places features: and semantic_drift: at the top level (and the loader reads them from the top level). Update the wording (or the example structure) so users aren’t misled about where these keys must live.
|
|
||
| The surveyor runs its 4-stage pipeline once and exits. As part of `alfred up`, it runs as a daemon with configurable intervals. | ||
|
|
||
| `alfred drift ...` commands are available only when `features.semantic_drift_monitor` is enabled. |
There was a problem hiding this comment.
This sentence is inaccurate: the alfred drift ... commands are still available when the feature flag is disabled; they just print a message and exit. Suggest rephrasing to clarify that drift artifacts/reports are only generated when enabled, and the CLI will report “disabled” otherwise.
| `alfred drift ...` commands are available only when `features.semantic_drift_monitor` is enabled. | |
| `alfred drift ...` commands are always available, but drift artifacts/reports are only generated when `features.semantic_drift_monitor` is enabled; otherwise, the CLI reports that semantic drift monitoring is disabled and exits. |
| print(f"Compared clusters: {latest['previous_cluster_count']} -> {latest['current_cluster_count']}") | ||
| print(f"Cluster delta: {latest['cluster_count_delta']}") | ||
| print(f"New clusters: {len(latest.get('new_clusters', []))}") | ||
| print(f"Dissolved clusters: {len(latest.get('dissolved_clusters', []))}") | ||
| print(f"Overall churn: {round(latest.get('overall_churn', 0.0) * 100, 2)}%") |
There was a problem hiding this comment.
cmd_status indexes required fields on latest using latest[...] (e.g., previous_cluster_count). If a drift report JSON is tampered with or partially written but still valid JSON, this will raise KeyError and crash the CLI. Prefer latest.get(...) with sensible defaults (and/or validate required keys after loading) so alfred drift status remains robust.
| def cmd_drift(args: argparse.Namespace) -> None: | ||
| raw = _load_unified_config(args.config) | ||
|
|
||
| if "surveyor" not in raw: | ||
| print("Surveyor is not configured in this config file.") | ||
| return | ||
|
|
||
| from alfred.surveyor.config import load_from_unified | ||
| from alfred.surveyor import drift_cli as dcli | ||
|
|
||
| config = load_from_unified(raw) |
There was a problem hiding this comment.
cmd_drift doesn’t call _setup_logging_from_config(raw), so structlog warnings from drift artifact parsing (and any other surveyor logging used by the drift CLI) may be missing or inconsistently formatted compared to other commands (e.g. alfred surveyor, alfred temporal). Consider initializing logging the same way here.
fix: deploy-ctrl CI permissions
Second of 5 commits for Voice Stage 2a-wk2. Extends the talker session schema with two new top-level fields and threads them through all close paths (explicit, timeout, startup sweep, shutdown). - session._build_session_frontmatter: adds session_type (default "note") and continues_from (default None) kwargs; emits both at the top level of the frontmatter so Dataview and vault search can filter directly. - session.close_session: new kwargs, written to the vault record AND to the closed_sessions state summary (plan open question ssdavidai#5 — state-only continuation lookup for wk2; body-parser fallback is wk3). - session.{resolve_on_startup,check_timeouts}: read _session_type / _continues_from off active dict with safe "note" / None defaults so wk1 state rehydrates cleanly (plan open question ssdavidai#2 — backwards-compat via .get). - bot._open_session_with_stash: accepts model/session_type/continues_from kwargs, stashes _session_type / _continues_from on the active dict. /end reads them back and passes them into close_session. - daemon shutdown path: reads both off active dict before close. Field name is "model" (not "model_used") — matches wk1 records so the two cohorts share one telemetry schema (plan open question ssdavidai#2). Tests: tests/telegram/test_session_frontmatter.py (2 tests). pytest: 24/24 passing (22 prior + 2 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-reviewer P1 ssdavidai#2 — release-blocker for Phase 1 Hypatia. `_validate_type` ran before `check_scope` on `vault_create`, gating against the canonical 20-type `KNOWN_TYPES` set only. Every Hypatia `document` / `concept` / `source` / `citation` / `template` and every KAL-LE `pattern` / `principle` create raised ``VaultError: Unknown type`` and never reached the scope-policy check — a misleading error that pointed at the wrong gate. The new `KNOWN_TYPES_KALLE` / `KNOWN_TYPES_HYPATIA` schema constants existed only to populate the scope's create-allowlists; nothing wired them into the type gate. Smoke check (master, pre-fix): $ ALFRED_VAULT_SCOPE=kalle alfred vault create pattern foo {"error": "Unknown type: 'pattern'. Valid: account, asset, ..."} $ ALFRED_VAULT_SCOPE=hypatia alfred vault create document foo {"error": "Unknown type: 'document'. Valid: account, asset, ..."} Smoke check (post-fix): pattern under kalle → SUCCESS, document under hypatia → SUCCESS, pattern under hypatia → "scope 'hypatia' can only create hypatia types" (caught by the second gate, not the first), document with no scope → "Unknown type" (canonical-only preserved for untriggered scopes). Implementation: * New `schema.KNOWN_TYPES_BY_SCOPE: dict[str, set[str]]` maps each extension scope to the union of canonical types plus its own set. Two-layer contract documented inline: this gate accepts the type; `check_scope`'s create-allowlist enforces the per-scope policy. * `_validate_type(record_type, scope=None)` consults the union when scope is set; default `None` preserves byte-for-byte behavior so every caller that doesn't propagate scope (talker `_execute_tool`, capture-extract, instructor) stays on canonical types only. * `vault_create` and `vault_list` thread their existing `scope` kwarg into the type gate. `vault/cli.py` already sources scope from `ALFRED_VAULT_SCOPE`; the create / list handlers now forward it through. CLI agent paths (curator, janitor, distiller, instructor's subprocess fallback, every external `alfred vault` invocation) get the fix automatically. Tests: 13 new cases in `test_hypatia_scope.py` exercise `vault_create` end-to-end — every Hypatia type, both KAL-LE types, cross-scope leak (document under kalle / pattern under hypatia both rejected at the type gate), Salem regression guard (note under talker still works), default scope=None still rejects extension types. Note: the talker `_execute_tool` in `telegram/conversation.py` still hardcodes `scope="talker"` — a separate Hypatia release-blocker for the LIVE bot path (vs the CLI agent path this commit unblocks). That one's a wider contract change (per-instance scope plumbing) and filed for a follow-up commit. Pattern-trigger note for `CLAUDE.md` Vault Operations Layer: "validation gate ordering" applies to every future instance with its own type set (V.E.R.A. RRTS, STAY-C). Add the scope's `KNOWN_TYPES_<NAME>` set AND a `KNOWN_TYPES_BY_SCOPE` entry, or the type gate silently rejects before scope enforcement runs.
This PR introduces an optional Semantic Drift Monitor for Surveyor, along with a focused round of security and robustness hardening.
The drift monitor adds temporal awareness to Surveyor’s clustering output by comparing cluster structure across runs. It does not alter clustering behavior or writeback logic. It is fully optional and disabled by default.
The security fixes address availability, path confinement, atomic writes, retention, and config validation.
What’s Included
1. Semantic Drift Monitor (Optional)
New module:
drift.py.logfilesSurveyor integration:
try/exceptto prevent pipeline failureCLI additions:
alfred drift statusalfred drift history [--limit N]alfred drift show <timestamp|filename>Configuration: