foxclaw 0.1.2 → 0.1.4
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/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -29,7 +29,7 @@ class _PriorityAdmission:
|
|
|
29
29
|
self.max = max_slots
|
|
30
30
|
self._free = max_slots or 0
|
|
31
31
|
self._cv = threading.Condition()
|
|
32
|
-
self._heap: list = []
|
|
32
|
+
self._heap: list = []
|
|
33
33
|
self._seq = 0
|
|
34
34
|
|
|
35
35
|
def acquire(self, priority: int = 0) -> None:
|
|
@@ -43,7 +43,7 @@ class _PriorityAdmission:
|
|
|
43
43
|
self._cv.wait()
|
|
44
44
|
heapq.heappop(self._heap)
|
|
45
45
|
self._free -= 1
|
|
46
|
-
if self._free > 0 and self._heap:
|
|
46
|
+
if self._free > 0 and self._heap:
|
|
47
47
|
self._cv.notify_all()
|
|
48
48
|
|
|
49
49
|
def release(self) -> None:
|
|
@@ -58,11 +58,8 @@ class Scheduler:
|
|
|
58
58
|
def __init__(self, max_concurrency: Optional[int] = None, spine=None,
|
|
59
59
|
process_manager=None):
|
|
60
60
|
self.max_concurrency = max_concurrency if (max_concurrency and max_concurrency > 0) else None
|
|
61
|
-
# Step5:优先级准入(满载时高优先排队者先获空位;不抢占正在跑的)。无上限时零等待。
|
|
62
61
|
self._adm = _PriorityAdmission(self.max_concurrency)
|
|
63
62
|
self._spine = spine
|
|
64
|
-
# 【Step3】统一生命周期:有 ProcessManager 则委托它发 task.lifecycle.*(唯一词汇、
|
|
65
|
-
# 唯一状态机);无 pm(standalone/测试)则调度器自发 task.lifecycle.* 作回退。
|
|
66
63
|
self.process_manager = process_manager
|
|
67
64
|
self._lock = threading.Lock()
|
|
68
65
|
self._stats = {"submitted": 0, "running": 0, "completed": 0,
|
|
@@ -80,7 +77,6 @@ class Scheduler:
|
|
|
80
77
|
proc = pm.create(task, agent_id=agent_id, schedule={"priority": priority})
|
|
81
78
|
else:
|
|
82
79
|
self._emit("scheduled", agent_id, task, priority)
|
|
83
|
-
# 优先级准入:满载则排队等空位(高优先先入);无上限时零等待(behavior-preserving)
|
|
84
80
|
self._adm.acquire(priority)
|
|
85
81
|
try:
|
|
86
82
|
with self._lock:
|
|
@@ -90,8 +86,6 @@ class Scheduler:
|
|
|
90
86
|
if proc is not None:
|
|
91
87
|
pm.mark_running(proc.process_id)
|
|
92
88
|
else:
|
|
93
|
-
# 【P5-2】回退路径的词汇与 ProcessManager 对齐:started → running
|
|
94
|
-
# (pm 的词汇是 created/queued/scheduled/running/completed/failed)
|
|
95
89
|
self._emit("running", agent_id, task, priority)
|
|
96
90
|
result = fn()
|
|
97
91
|
with self._lock:
|
|
@@ -116,7 +110,7 @@ class Scheduler:
|
|
|
116
110
|
|
|
117
111
|
def stats(self) -> dict:
|
|
118
112
|
with self._lock:
|
|
119
|
-
s = dict(self._stats)
|
|
113
|
+
s: dict[str, int | None] = dict(self._stats)
|
|
120
114
|
s["max_concurrency"] = self.max_concurrency
|
|
121
115
|
return s
|
|
122
116
|
|
|
@@ -23,12 +23,9 @@ import secrets
|
|
|
23
23
|
import sys
|
|
24
24
|
import time
|
|
25
25
|
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
# ── 凭证保险箱(vault)────────────────────────────────────────
|
|
29
|
-
# 真实凭证只进守门人进程,主进程内存里从不出现。两个来源:
|
|
30
|
-
# 1. 环境变量前缀 FOXCLAW_GUARDIAN_SECRET_<REF>(REF 统一小写);
|
|
31
|
-
# 2. FOXCLAW_GUARDIAN_SECRETS 指向的 JSON 文件 {"ref": "secret", ...}。
|
|
32
29
|
def load_vault() -> dict:
|
|
33
30
|
vault: dict = {}
|
|
34
31
|
prefix = "FOXCLAW_GUARDIAN_SECRET_"
|
|
@@ -42,13 +39,10 @@ def load_vault() -> dict:
|
|
|
42
39
|
if isinstance(d, dict):
|
|
43
40
|
vault.update({str(k).lower(): v for k, v in d.items()})
|
|
44
41
|
except Exception:
|
|
45
|
-
pass
|
|
42
|
+
pass
|
|
46
43
|
return vault
|
|
47
44
|
|
|
48
45
|
|
|
49
|
-
# ── MCP server 配置(守门人持有凭证)──────────────────────────
|
|
50
|
-
# 与 vault 对称:MCP 的 headers 里可能含真凭证(Authorization/x-api-key 等)。
|
|
51
|
-
# 主进程通过 mcp_register 推送配置;也可用 FOXCLAW_GUARDIAN_MCP_SERVERS 指向 JSON 文件做初始加载。
|
|
52
46
|
def _mcp_servers_path() -> str:
|
|
53
47
|
"""守门人侧 MCP 配置持久化文件。优先 FOXCLAW_GUARDIAN_MCP_SERVERS 显式指定;
|
|
54
48
|
否则用 FOXCLAW_GUARDIAN_DATA_DIR 下的 mcp_guardian_servers.json。主进程的 mcp_servers.json 已不含 headers。"""
|
|
@@ -79,7 +73,7 @@ def load_mcp_servers() -> dict:
|
|
|
79
73
|
"transport": c.get("transport") or "streamable_http",
|
|
80
74
|
}
|
|
81
75
|
except Exception:
|
|
82
|
-
pass
|
|
76
|
+
pass
|
|
83
77
|
return servers
|
|
84
78
|
|
|
85
79
|
|
|
@@ -94,18 +88,15 @@ def _persist_mcp_servers(servers: dict) -> None:
|
|
|
94
88
|
tmp.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
95
89
|
tmp.replace(Path(path))
|
|
96
90
|
except Exception:
|
|
97
|
-
pass
|
|
91
|
+
pass
|
|
98
92
|
|
|
99
93
|
|
|
100
|
-
# ── 短期可撤销能力令牌 ──────────────────────────────────────
|
|
101
|
-
# 彻底取代「resolve 真值借出」:主进程只拿得到一个不透明随机 token,真值永不出守门人进程。
|
|
102
|
-
# 令牌 = 一次性 + 过期 + 可撤销,进程内状态表(不依赖 HMAC 签名,因为 token 本身高熵、无法伪造/猜中)。
|
|
103
94
|
class TokenStore:
|
|
104
95
|
def __init__(self, default_ttl: float = 60.0):
|
|
105
96
|
self._default_ttl = default_ttl
|
|
106
97
|
self._tokens: dict = {}
|
|
107
98
|
|
|
108
|
-
def mint(self, ref: str, ttl: float = None) -> dict:
|
|
99
|
+
def mint(self, ref: str, ttl: float | None = None) -> dict:
|
|
109
100
|
"""为某 ref 签发能力令牌。返回 {token, expires_in},绝不返回真值。"""
|
|
110
101
|
ttl = ttl if ttl is not None else self._default_ttl
|
|
111
102
|
token = secrets.token_urlsafe(32)
|
|
@@ -137,7 +128,6 @@ class TokenStore:
|
|
|
137
128
|
return True
|
|
138
129
|
|
|
139
130
|
|
|
140
|
-
# ── 跨进程重建(守门人侧)─────────────────────────────────────
|
|
141
131
|
def _rebuild_skill(sd: dict):
|
|
142
132
|
"""主进程只传 evaluate 需要的字段 + 已算好的 trust。重建为鸭子类型对象。"""
|
|
143
133
|
from types import SimpleNamespace
|
|
@@ -145,7 +135,7 @@ def _rebuild_skill(sd: dict):
|
|
|
145
135
|
return SimpleNamespace(
|
|
146
136
|
id=sd.get("id", ""),
|
|
147
137
|
kind=sd.get("kind", "local"),
|
|
148
|
-
side_effect=sd.get("side_effect", "write"),
|
|
138
|
+
side_effect=sd.get("side_effect", "write"),
|
|
149
139
|
destructive=bool(sd.get("destructive", False)),
|
|
150
140
|
external=bool(sd.get("external", False)),
|
|
151
141
|
read_only=sd.get("read_only", None),
|
|
@@ -187,7 +177,7 @@ def _handle_mint(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
187
177
|
ttl = float(ttl) if ttl else None
|
|
188
178
|
out = store.mint(ref, ttl)
|
|
189
179
|
print(f"[Sentinel] mint {ref}", file=sys.stderr, flush=True)
|
|
190
|
-
return out
|
|
180
|
+
return out
|
|
191
181
|
|
|
192
182
|
|
|
193
183
|
def _handle_revoke(msg: dict, store: TokenStore) -> dict:
|
|
@@ -212,7 +202,7 @@ def _handle_mcp_register(msg: dict, servers: dict) -> dict:
|
|
|
212
202
|
headers = dict(c.get("headers") or {})
|
|
213
203
|
old = servers.get(sid)
|
|
214
204
|
if not headers and old:
|
|
215
|
-
headers = dict(old.get("headers") or {})
|
|
205
|
+
headers = dict(old.get("headers") or {})
|
|
216
206
|
new_servers[sid] = {
|
|
217
207
|
"name": c.get("name") or sid,
|
|
218
208
|
"url": c.get("url", ""),
|
|
@@ -251,7 +241,7 @@ def _handle_mcp_call(msg: dict, servers: dict) -> dict:
|
|
|
251
241
|
if out.get("isError"):
|
|
252
242
|
return SkillResult(skill_id, False, status="blocked",
|
|
253
243
|
message=f"MCP 工具返回错误: {out.get('content') or 'unknown'}").to_dict()
|
|
254
|
-
out = _mark_untrusted(out)
|
|
244
|
+
out = _mark_untrusted(out)
|
|
255
245
|
print(f"[Sentinel] mcp_call {server_id}.{tool}", file=sys.stderr, flush=True)
|
|
256
246
|
return SkillResult(skill_id, True, output=out,
|
|
257
247
|
message="MCP 调用完成(外部不可信数据)").to_dict()
|
|
@@ -329,7 +319,7 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
329
319
|
secret = None
|
|
330
320
|
used_ref = None
|
|
331
321
|
if token:
|
|
332
|
-
ref = store.redeem(token)
|
|
322
|
+
ref = store.redeem(token)
|
|
333
323
|
if ref is None:
|
|
334
324
|
return SkillResult(skill_id, False, status="blocked",
|
|
335
325
|
message="能力令牌无效/已过期/已使用/已撤销").to_dict()
|
|
@@ -345,7 +335,6 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
345
335
|
return SkillResult(skill_id, False, status="blocked",
|
|
346
336
|
message=f"凭证 ref「{secret_ref}」未在守门人 vault 中配置").to_dict()
|
|
347
337
|
|
|
348
|
-
# SSRF 校验:守门人侧也做(主进程可能被攻破传内网地址,不能只信主进程)
|
|
349
338
|
block = ssrf_check(endpoint, allow_local=allow_local)
|
|
350
339
|
if block:
|
|
351
340
|
return SkillResult(skill_id, False, status="blocked",
|
|
@@ -364,7 +353,7 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
364
353
|
return SkillResult(skill_id, False, status="blocked",
|
|
365
354
|
message="缺少 httpx 依赖,无法发起 Web API 调用").to_dict()
|
|
366
355
|
|
|
367
|
-
req_kwargs = {"headers": headers, "timeout": timeout, "follow_redirects": False}
|
|
356
|
+
req_kwargs: dict[str, Any] = {"headers": headers, "timeout": timeout, "follow_redirects": False}
|
|
368
357
|
if loc == "query":
|
|
369
358
|
req_kwargs["params"] = args
|
|
370
359
|
else:
|
|
@@ -416,6 +405,7 @@ def serve_stdio(vault: dict, store: TokenStore, servers: dict) -> None:
|
|
|
416
405
|
mid = msg.get("id")
|
|
417
406
|
op = msg.get("op")
|
|
418
407
|
try:
|
|
408
|
+
result: dict[str, object]
|
|
419
409
|
if op == "ping":
|
|
420
410
|
result = {"pong": True}
|
|
421
411
|
elif op == "evaluate":
|
|
@@ -438,7 +428,7 @@ def serve_stdio(vault: dict, store: TokenStore, servers: dict) -> None:
|
|
|
438
428
|
else:
|
|
439
429
|
result = {"error": f"unknown op: {op}"}
|
|
440
430
|
_reply({"id": mid, "ok": True, "result": result})
|
|
441
|
-
except Exception as e:
|
|
431
|
+
except Exception as e:
|
|
442
432
|
_reply({"id": mid, "ok": False, "error": str(e)})
|
|
443
433
|
|
|
444
434
|
|
|
@@ -22,20 +22,18 @@ class SentinelUnavailable(RuntimeError):
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _project_root() -> str:
|
|
25
|
-
# foxclaw/kernel/sentinel_client.py → parents[0]=kernel, [1]=foxclaw, [2]=Foxclaw
|
|
26
25
|
return str(Path(__file__).resolve().parents[2])
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
class SentinelClient:
|
|
30
|
-
def __init__(self, timeout: float = 5.0, cmd=None, data_dir: str = None):
|
|
29
|
+
def __init__(self, timeout: float = 5.0, cmd=None, data_dir: str | None = None):
|
|
31
30
|
self._timeout = timeout
|
|
32
31
|
self._data_dir = data_dir
|
|
33
32
|
self._lock = threading.Lock()
|
|
34
33
|
self._req_id = 0
|
|
35
|
-
self._proc = None
|
|
34
|
+
self._proc: subprocess.Popen[str] | None = None
|
|
36
35
|
self._spawn(cmd)
|
|
37
36
|
|
|
38
|
-
# ── 进程生命周期 ─────────────────────────────────────────
|
|
39
37
|
def _spawn(self, cmd) -> None:
|
|
40
38
|
if cmd is None:
|
|
41
39
|
cmd = [sys.executable, "-m", "foxclaw.kernel.sentinel"]
|
|
@@ -50,7 +48,7 @@ class SentinelClient:
|
|
|
50
48
|
except Exception as e:
|
|
51
49
|
raise RuntimeError(f"Sentinel 启动失败: {e}")
|
|
52
50
|
try:
|
|
53
|
-
self._request("ping", {})
|
|
51
|
+
self._request("ping", {})
|
|
54
52
|
except Exception as e:
|
|
55
53
|
self._kill()
|
|
56
54
|
raise RuntimeError(f"Sentinel 握手失败: {e}")
|
|
@@ -73,16 +71,18 @@ class SentinelClient:
|
|
|
73
71
|
except Exception:
|
|
74
72
|
pass
|
|
75
73
|
|
|
76
|
-
# ── IPC 核心 ─────────────────────────────────────────────
|
|
77
74
|
def _request(self, op: str, payload: dict):
|
|
78
75
|
with self._lock:
|
|
79
76
|
self._req_id += 1
|
|
80
77
|
mid = self._req_id
|
|
81
78
|
msg = json.dumps({"id": mid, "op": op, **payload}, ensure_ascii=False)
|
|
82
79
|
try:
|
|
83
|
-
self._proc
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
proc = self._proc
|
|
81
|
+
if proc is None or proc.stdin is None or proc.stdout is None:
|
|
82
|
+
raise SentinelUnavailable("守门人进程未启动")
|
|
83
|
+
proc.stdin.write(msg + "\n")
|
|
84
|
+
proc.stdin.flush()
|
|
85
|
+
line = proc.stdout.readline()
|
|
86
86
|
except Exception as e:
|
|
87
87
|
raise SentinelUnavailable(f"IPC 通信失败: {e}")
|
|
88
88
|
if not line:
|
|
@@ -95,7 +95,6 @@ class SentinelClient:
|
|
|
95
95
|
raise SentinelUnavailable(resp.get("error", "unknown error"))
|
|
96
96
|
return resp.get("result")
|
|
97
97
|
|
|
98
|
-
# ── 序列化(只传 evaluate 需要的字段,不含 handler/凭证)────
|
|
99
98
|
@staticmethod
|
|
100
99
|
def _serialize_skill(skill) -> dict:
|
|
101
100
|
_tl = getattr(skill, "trust_level", None)
|
|
@@ -106,7 +105,7 @@ class SentinelClient:
|
|
|
106
105
|
return {
|
|
107
106
|
"id": getattr(skill, "id", ""),
|
|
108
107
|
"kind": getattr(skill, "kind", "local"),
|
|
109
|
-
"side_effect": getattr(skill, "side_effect", "write"),
|
|
108
|
+
"side_effect": getattr(skill, "side_effect", "write"),
|
|
110
109
|
"destructive": bool(getattr(skill, "destructive", False)),
|
|
111
110
|
"external": bool(getattr(skill, "external", False)),
|
|
112
111
|
"read_only": getattr(skill, "read_only", None),
|
|
@@ -124,7 +123,6 @@ class SentinelClient:
|
|
|
124
123
|
"allow": list(getattr(rules, "allow", []) or []),
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
# ── 对外接口 ─────────────────────────────────────────────
|
|
128
126
|
def evaluate(self, skill, args, rules=None, confirmed=False, mode="strict",
|
|
129
127
|
taint_level=0, taint_threshold=0):
|
|
130
128
|
"""动作裁决(跨进程)。签名对齐 skills.permission.evaluate,返回 PermissionDecision。"""
|
|
@@ -141,7 +139,6 @@ class SentinelClient:
|
|
|
141
139
|
try:
|
|
142
140
|
d = self._request("evaluate", payload)
|
|
143
141
|
except SentinelUnavailable as e:
|
|
144
|
-
# fail-closed:守门人不可达 → deny,绝不降级放行
|
|
145
142
|
return PermissionDecision("deny", 0,
|
|
146
143
|
reason=f"守门人不可达(fail-closed): {e}",
|
|
147
144
|
reason_code="SENTINEL_UNAVAILABLE")
|
|
@@ -154,9 +151,9 @@ class SentinelClient:
|
|
|
154
151
|
reason_code=d.get("reason_code", ""),
|
|
155
152
|
)
|
|
156
153
|
|
|
157
|
-
def mint(self, ref: str, ttl: float = None):
|
|
154
|
+
def mint(self, ref: str, ttl: float | None = None):
|
|
158
155
|
"""为 ref 签发一次性能力令牌。返回 token(绝不返回真值);失败返回 None。"""
|
|
159
|
-
payload = {"ref": ref}
|
|
156
|
+
payload: dict[str, object] = {"ref": ref}
|
|
160
157
|
if ttl is not None:
|
|
161
158
|
payload["ttl"] = ttl
|
|
162
159
|
try:
|
|
@@ -32,7 +32,6 @@ class Event:
|
|
|
32
32
|
source: str = ""
|
|
33
33
|
correlation_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
34
34
|
|
|
35
|
-
# ── 与旧 BusMessage 同形,便于 SystemBus 无痛门面化 ──
|
|
36
35
|
@property
|
|
37
36
|
def channel(self) -> str:
|
|
38
37
|
return self.topic
|
|
@@ -46,8 +45,8 @@ class EventSpine:
|
|
|
46
45
|
"""进程内唯一事件脊柱。发布/订阅(前缀匹配) + 投影 + 有界历史。线程安全。"""
|
|
47
46
|
|
|
48
47
|
def __init__(self, history_limit: int = 1000):
|
|
49
|
-
self._subs: list[tuple[str, Callable]] = []
|
|
50
|
-
self._projections: list[Callable] = []
|
|
48
|
+
self._subs: list[tuple[str, Callable]] = []
|
|
49
|
+
self._projections: list[Callable] = []
|
|
51
50
|
self._history: deque = deque(maxlen=history_limit)
|
|
52
51
|
self._lock = threading.RLock()
|
|
53
52
|
|
|
@@ -61,13 +60,15 @@ class EventSpine:
|
|
|
61
60
|
with self._lock:
|
|
62
61
|
self._projections.append(sink)
|
|
63
62
|
|
|
64
|
-
def publish(self, topic: str, payload: dict = None, source: str = "",
|
|
65
|
-
correlation_id: str = None) -> Event:
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
def publish(self, topic: str, payload: dict | None = None, source: str = "",
|
|
64
|
+
correlation_id: str | None = None) -> Event:
|
|
65
|
+
if correlation_id:
|
|
66
|
+
evt = Event(topic=topic, payload=payload or {}, source=source,
|
|
67
|
+
correlation_id=correlation_id)
|
|
68
|
+
else:
|
|
69
|
+
evt = Event(topic=topic, payload=payload or {}, source=source)
|
|
68
70
|
with self._lock:
|
|
69
71
|
self._history.append(evt)
|
|
70
|
-
# 锁内只做"快照",handler/sink 在锁外执行(避免与 pm/hmr 锁形成环)
|
|
71
72
|
handlers = [h for pfx, h in self._subs if topic.startswith(pfx)]
|
|
72
73
|
projections = list(self._projections)
|
|
73
74
|
for h in handlers:
|
|
@@ -90,7 +91,6 @@ class EventSpine:
|
|
|
90
91
|
|
|
91
92
|
def stats(self) -> dict:
|
|
92
93
|
with self._lock:
|
|
93
|
-
# 【P4-2】补 history_limit(deque.maxlen):只报 history_size 无法判断「是否已满」。
|
|
94
94
|
return {"subscriptions": len(self._subs),
|
|
95
95
|
"projections": len(self._projections),
|
|
96
96
|
"history_size": len(self._history),
|
|
@@ -64,7 +64,6 @@ def _read_custom_model(data_dir, name):
|
|
|
64
64
|
if not match:
|
|
65
65
|
return None
|
|
66
66
|
key = ""
|
|
67
|
-
# OS 凭据库优先,回退 base64(旧记录)/ 更早的明文字段
|
|
68
67
|
from ..core.secrets import load as _load_secret
|
|
69
68
|
key = _load_secret(name, match.get("api_key_obf") or "")
|
|
70
69
|
key = key or (match.get("api_key") or "")
|
|
@@ -101,7 +100,6 @@ def _selected_llm():
|
|
|
101
100
|
return None
|
|
102
101
|
|
|
103
102
|
source = (sel.get("source") or "").strip().lower()
|
|
104
|
-
# preset 落盘的是 provider;custom 落盘的是 name。两者都当"选中的名字"用。
|
|
105
103
|
name = (sel.get("name") or sel.get("provider") or "").strip()
|
|
106
104
|
model = (sel.get("model_id") or sel.get("model") or "").strip()
|
|
107
105
|
|
|
@@ -113,7 +111,6 @@ def _selected_llm():
|
|
|
113
111
|
return _to_hkc_provider("generic"), key, base, model or custom_model
|
|
114
112
|
|
|
115
113
|
if source == "preset" and name:
|
|
116
|
-
# 界面里配过的同名自定义项(更新的 key)优先,env 只作最后兜底
|
|
117
114
|
got = _read_custom_model(data_dir, name)
|
|
118
115
|
if got:
|
|
119
116
|
key, base, custom_model = got
|
|
@@ -144,7 +141,6 @@ def _detect_llm(api_key=None):
|
|
|
144
141
|
|
|
145
142
|
返回 ``(provider, key, base_url, model)``。
|
|
146
143
|
"""
|
|
147
|
-
# 1. 显式 HKC_LLM_PROVIDER 覆盖(向后兼容「HKC 独立配」场景)
|
|
148
144
|
env_provider = (os.environ.get("HKC_LLM_PROVIDER") or "").strip().lower()
|
|
149
145
|
if env_provider in ("anthropic", "deepseek", "openai", "openai-compatible"):
|
|
150
146
|
key = (os.environ.get("HKC_LLM_API_KEY")
|
|
@@ -158,15 +154,11 @@ def _detect_llm(api_key=None):
|
|
|
158
154
|
or "")
|
|
159
155
|
return env_provider, key or api_key, base, model
|
|
160
156
|
|
|
161
|
-
# 2. 界面里当前选中的模型(用户显式动作,优先于 env 默认)
|
|
162
157
|
selected = _selected_llm()
|
|
163
158
|
if selected:
|
|
164
159
|
provider, key, base, model = selected
|
|
165
160
|
return provider, (key or api_key or "").strip(), (base or "").strip(), model
|
|
166
161
|
|
|
167
|
-
# 3. 单入口 env。未显式指定 provider 时**复用主对话的自动探测**,
|
|
168
|
-
# 不能默认成 "generic" —— 那是个空 key 的壳,会让抽取打 api.openai.com
|
|
169
|
-
# 拿 Missing credentials,每次摄入都 0 条。
|
|
170
162
|
provider = (os.environ.get("LLM_PROVIDER") or "").strip().lower()
|
|
171
163
|
key = (os.environ.get("LLM_API_KEY") or api_key or "").strip()
|
|
172
164
|
base = os.environ.get("LLM_BASE_URL", "").strip()
|
|
@@ -194,7 +186,7 @@ class HKCService:
|
|
|
194
186
|
|
|
195
187
|
def __init__(self, container, client):
|
|
196
188
|
self._container = container
|
|
197
|
-
self.client = client
|
|
189
|
+
self.client = client
|
|
198
190
|
|
|
199
191
|
@property
|
|
200
192
|
def available(self) -> bool:
|
|
@@ -245,22 +237,18 @@ def init_hkc(data_dir, api_key=None):
|
|
|
245
237
|
return None
|
|
246
238
|
try:
|
|
247
239
|
provider, llm_key, llm_base_url, llm_model = _detect_llm(api_key)
|
|
248
|
-
# 【P2·单 LLM 入口】embedding 模型名优先 EMBEDDING_MODEL(系统唯一本地入口),
|
|
249
|
-
# 再 HKC_EMBEDDING_MODEL(向后兼容);其他 HKC 行为开关不变。
|
|
250
240
|
_emb_model = (os.environ.get("EMBEDDING_MODEL")
|
|
251
241
|
or os.environ.get("HKC_EMBEDDING_MODEL")) or None
|
|
252
242
|
container = HKCContainer(
|
|
253
243
|
data_dir=str(Path(data_dir) / "hkc"),
|
|
254
|
-
embedding_kind=os.environ.get("HKC_EMBEDDING", "auto"),
|
|
244
|
+
embedding_kind=os.environ.get("HKC_EMBEDDING", "auto"),
|
|
255
245
|
embedding_url=os.environ.get("HKC_EMBEDDING_URL", "http://localhost:8080"),
|
|
256
|
-
embedding_model=_emb_model,
|
|
246
|
+
embedding_model=_emb_model,
|
|
257
247
|
llm_api_key=llm_key or None,
|
|
258
248
|
llm_provider=provider,
|
|
259
249
|
llm_model=llm_model or None,
|
|
260
250
|
llm_base_url=llm_base_url or None,
|
|
261
251
|
).assemble()
|
|
262
|
-
# 注册为 hkc_api 全局单例:若将来挂 HKC 自身 /hkc REST 路由,
|
|
263
|
-
# 其 Depends(get_container) 能取到这同一个容器,而非"未初始化"。
|
|
264
252
|
from hkc_api import container as _cm
|
|
265
253
|
_cm._container = container
|
|
266
254
|
print(f"[Foxclaw] ✓ HKC 知识核心已进程内融合 (provider={provider}, "
|
|
@@ -275,8 +263,6 @@ def init_hkc(data_dir, api_key=None):
|
|
|
275
263
|
from .truth_crystallizer import TruthCrystallizer
|
|
276
264
|
|
|
277
265
|
|
|
278
|
-
# HKC 知识生命周期事件 → 桥接到 EventSpine 的类型。**刻意不含 memory.***(那是 HMR 域,
|
|
279
|
-
# Foxclaw 已有自己的记忆事件,转发 HKC 的会重复/混淆)。
|
|
280
266
|
_HKC_BRIDGED_EVENTS = (
|
|
281
267
|
"knowledge.created", "knowledge.updated", "knowledge.merged", "knowledge.deleted",
|
|
282
268
|
"conflict.detected", "conflict.resolved", "claim.status_changed",
|
|
@@ -351,7 +337,7 @@ def register_abilities_as_skills(registry, hkc) -> list:
|
|
|
351
337
|
registered = []
|
|
352
338
|
for key in keys:
|
|
353
339
|
try:
|
|
354
|
-
ab = client.compile_ability(key)
|
|
340
|
+
ab = client.compile_ability(key)
|
|
355
341
|
except Exception:
|
|
356
342
|
continue
|
|
357
343
|
sid = f"ability:{key}"
|
|
@@ -363,8 +349,6 @@ def register_abilities_as_skills(registry, hkc) -> list:
|
|
|
363
349
|
handler=_make_ability_handler(client, ab),
|
|
364
350
|
params={"query": "str"}))
|
|
365
351
|
registered.append(sid)
|
|
366
|
-
# 【深审修复·陈旧清理】重新同步时,移除此前注册但**本轮不再达标**的 ability skill
|
|
367
|
-
# (coverage 掉了/能力定义删了)——register-only 会残留陈旧项;prune 使 registry 反映当前覆盖。
|
|
368
352
|
fresh = set(registered)
|
|
369
353
|
for spec in registry.all():
|
|
370
354
|
if spec.id.startswith("ability:") and spec.id not in fresh:
|
|
@@ -15,17 +15,17 @@ Step1(叠加、默认关、行为不变):
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
import threading
|
|
17
17
|
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
-
from typing import Callable, Optional
|
|
18
|
+
from typing import Any, Callable, cast, Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class TruthCrystallizer:
|
|
22
22
|
def __init__(self, hkc_provider: Callable[[], object], enabled: bool = False):
|
|
23
|
-
self._hkc_provider = hkc_provider
|
|
23
|
+
self._hkc_provider = hkc_provider
|
|
24
24
|
self.enabled = enabled
|
|
25
25
|
self._executor: Optional[ThreadPoolExecutor] = None
|
|
26
26
|
self._closed = False
|
|
27
27
|
self._lock = threading.Lock()
|
|
28
|
-
self._pending: list = []
|
|
28
|
+
self._pending: list = []
|
|
29
29
|
|
|
30
30
|
def _ex(self) -> Optional[ThreadPoolExecutor]:
|
|
31
31
|
with self._lock:
|
|
@@ -41,7 +41,7 @@ class TruthCrystallizer:
|
|
|
41
41
|
if not self.enabled:
|
|
42
42
|
return
|
|
43
43
|
p = getattr(msg, "payload", None) or {}
|
|
44
|
-
if p.get("blocked"):
|
|
44
|
+
if p.get("blocked"):
|
|
45
45
|
return
|
|
46
46
|
fact = ((p.get("truth") or {}).get("fact") or "").strip()
|
|
47
47
|
if not fact:
|
|
@@ -52,16 +52,13 @@ class TruthCrystallizer:
|
|
|
52
52
|
return
|
|
53
53
|
fut = ex.submit(self._crystallize, fact, domain)
|
|
54
54
|
with self._lock:
|
|
55
|
-
# 【深审修复·长期运行泄漏】_pending 原先只增不减(drain 仅在 close/测试时调用),
|
|
56
|
-
# 启用后每来一次 truth.appended 就永久累积一个已完成 Future → 无界内存泄漏。
|
|
57
|
-
# 改为每次提交时 prune 掉已完成的,_pending 只保留在飞行中的任务(仍支持测试 drain)。
|
|
58
55
|
self._pending = [f for f in self._pending if not f.done()]
|
|
59
56
|
self._pending.append(fut)
|
|
60
57
|
|
|
61
58
|
def _crystallize(self, fact: str, domain: str) -> bool:
|
|
62
59
|
"""后台执行:惰性取 HKC → ingest_text(KDE→KEE)。任何失败都吞掉,不影响主流程。"""
|
|
63
60
|
try:
|
|
64
|
-
hkc = self._hkc_provider()
|
|
61
|
+
hkc = cast(Any, self._hkc_provider())
|
|
65
62
|
except Exception:
|
|
66
63
|
hkc = None
|
|
67
64
|
if hkc is None or not getattr(hkc, "available", False):
|
|
@@ -17,9 +17,9 @@ class KnowledgeGraph:
|
|
|
17
17
|
DIM = 256
|
|
18
18
|
|
|
19
19
|
def __init__(self, embedder=None):
|
|
20
|
-
self.items: dict = {}
|
|
21
|
-
self.edges: list = []
|
|
22
|
-
self._embedder = embedder
|
|
20
|
+
self.items: dict = {}
|
|
21
|
+
self.edges: list = []
|
|
22
|
+
self._embedder = embedder
|
|
23
23
|
|
|
24
24
|
def add_item(self, item: KnowledgeItem):
|
|
25
25
|
self.items[item.id] = item
|
|
@@ -30,7 +30,6 @@ class KnowledgeGraph:
|
|
|
30
30
|
self.edges = [e for e in self.edges
|
|
31
31
|
if e.source_id != item_id and e.target_id != item_id]
|
|
32
32
|
|
|
33
|
-
# ── 三种连接 ──────────────────────────────────────────
|
|
34
33
|
def link_manual(self, source_id: str, target_id: str, reason: str = ""):
|
|
35
34
|
self.edges.append(KnowledgeEdge(
|
|
36
35
|
source_id=source_id, target_id=target_id,
|
|
@@ -72,7 +71,6 @@ class KnowledgeGraph:
|
|
|
72
71
|
new_edges += 1
|
|
73
72
|
return new_edges
|
|
74
73
|
|
|
75
|
-
# ── 图分析 ────────────────────────────────────────────
|
|
76
74
|
def neighbors(self, item_id: str) -> list:
|
|
77
75
|
"""某知识点的所有关联点。"""
|
|
78
76
|
out = []
|
|
@@ -105,7 +103,7 @@ class KnowledgeGraph:
|
|
|
105
103
|
return result
|
|
106
104
|
|
|
107
105
|
def stats(self) -> dict:
|
|
108
|
-
type_count = defaultdict(int)
|
|
106
|
+
type_count: defaultdict[str, int] = defaultdict(int)
|
|
109
107
|
for e in self.edges:
|
|
110
108
|
type_count[e.edge_type] += 1
|
|
111
109
|
return {
|
|
@@ -115,7 +113,6 @@ class KnowledgeGraph:
|
|
|
115
113
|
"clusters": len(self.clusters()),
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
# ── 向量工具 ──────────────────────────────────────────
|
|
119
116
|
def _embed(self, text: str):
|
|
120
117
|
if self._embedder is not None:
|
|
121
118
|
try:
|
|
@@ -138,8 +135,6 @@ class KnowledgeGraph:
|
|
|
138
135
|
|
|
139
136
|
@staticmethod
|
|
140
137
|
def _cosine(a, b) -> float:
|
|
141
|
-
# 自己归一化,不假设输入已是单位向量:外部 embedder(如 HMR)返回的向量
|
|
142
|
-
# 未必归一化,直接用点积当余弦会系统性偏高(threshold 失效,什么都连上)。
|
|
143
138
|
import math
|
|
144
139
|
dot = sum(x * y for x, y in zip(a, b))
|
|
145
140
|
na = math.sqrt(sum(x * x for x in a))
|
|
@@ -13,17 +13,17 @@ import uuid
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
EdgeType = Literal["same_topic", "concept_ref", "semantic", "manual"]
|
|
16
|
-
Grade = Literal[0, 1, 2, 3, 4, 5]
|
|
16
|
+
Grade = Literal[0, 1, 2, 3, 4, 5]
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@dataclass
|
|
20
20
|
class KnowledgeItem:
|
|
21
21
|
"""一个知识条目 = 一次学习采集"""
|
|
22
|
-
content: str
|
|
23
|
-
topic: str = "general"
|
|
24
|
-
source: str = ""
|
|
22
|
+
content: str
|
|
23
|
+
topic: str = "general"
|
|
24
|
+
source: str = ""
|
|
25
25
|
tags: list = field(default_factory=list)
|
|
26
|
-
notes: list = field(default_factory=list)
|
|
26
|
+
notes: list = field(default_factory=list)
|
|
27
27
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
28
28
|
id: str = field(default_factory=lambda: f"ki_{uuid.uuid4().hex[:10]}")
|
|
29
29
|
|
|
@@ -35,19 +35,19 @@ class KnowledgeItem:
|
|
|
35
35
|
class ReviewState:
|
|
36
36
|
"""SM-2 间隔重复状态(固化层核心)"""
|
|
37
37
|
item_id: str
|
|
38
|
-
repetitions: int = 0
|
|
39
|
-
interval: int = 0
|
|
40
|
-
ease_factor: float = 2.5
|
|
38
|
+
repetitions: int = 0
|
|
39
|
+
interval: int = 0
|
|
40
|
+
ease_factor: float = 2.5
|
|
41
41
|
due_date: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
42
42
|
last_review: Optional[str] = None
|
|
43
43
|
total_reviews: int = 0
|
|
44
|
-
lapses: int = 0
|
|
44
|
+
lapses: int = 0
|
|
45
45
|
|
|
46
46
|
@property
|
|
47
47
|
def mastery(self) -> float:
|
|
48
48
|
"""掌握度 0~1:综合连续正确次数和难度因子。"""
|
|
49
|
-
rep_score = min(self.repetitions / 6.0, 1.0)
|
|
50
|
-
ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3)
|
|
49
|
+
rep_score = min(self.repetitions / 6.0, 1.0)
|
|
50
|
+
ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3)
|
|
51
51
|
ease_score = max(0.0, min(1.0, ease_score))
|
|
52
52
|
return round(0.6 * rep_score + 0.4 * ease_score, 3)
|
|
53
53
|
|
|
@@ -73,8 +73,8 @@ class KnowledgeEdge:
|
|
|
73
73
|
source_id: str
|
|
74
74
|
target_id: str
|
|
75
75
|
edge_type: EdgeType
|
|
76
|
-
weight: float = 1.0
|
|
77
|
-
reason: str = ""
|
|
76
|
+
weight: float = 1.0
|
|
77
|
+
reason: str = ""
|
|
78
78
|
|
|
79
79
|
def to_dict(self) -> dict:
|
|
80
80
|
return asdict(self)
|
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
|
|
12
12
|
from .spaced_repetition import SpacedRepetition
|
|
13
13
|
|
|
14
|
-
# 负荷偏重阈值:未来窗口日均复习量 ≥ 该值即算偏重,交给可选 LLM 复核是否提醒。
|
|
15
14
|
HEAVY_DAILY = 20.0
|
|
16
15
|
|
|
17
16
|
|
|
@@ -37,13 +36,12 @@ class LearningReviewReasoner(DomainReasoner):
|
|
|
37
36
|
return h or ["学习状态可能健康"]
|
|
38
37
|
|
|
39
38
|
def gather_evidence(self, states, obs: dict) -> list:
|
|
40
|
-
risky = SpacedRepetition.forgetting_risk(states, self.horizon_days)
|
|
39
|
+
risky = SpacedRepetition.forgetting_risk(states, self.horizon_days)
|
|
41
40
|
ev = [Evidence(factor="forgetting_risk",
|
|
42
41
|
detail=r["reason"] + f"(item {r['item_id']})", weight=1.0, raw=r)
|
|
43
42
|
for r in risky]
|
|
44
43
|
if not risky:
|
|
45
44
|
ev.append(Evidence("no_urgent_risk", "无即将到期且掌握度低的知识点", 0.5))
|
|
46
|
-
# informational(raw=None):学习负荷,不参与裁决
|
|
47
45
|
ev.append(Evidence("learning_load",
|
|
48
46
|
f"未来窗口共 {obs['total_due']} 项待复习,日均 {obs['daily_avg']}", 0.3))
|
|
49
47
|
return ev
|
|
@@ -51,7 +49,6 @@ class LearningReviewReasoner(DomainReasoner):
|
|
|
51
49
|
def arbitrate(self, states, obs, hyps, ev) -> ReasonedVerdict:
|
|
52
50
|
triggered = [e for e in ev if e.raw is not None]
|
|
53
51
|
if triggered:
|
|
54
|
-
# 【规则权威】存在高遗忘风险 → confirm(建议立即复习),绝不被 LLM 推翻/隐藏
|
|
55
52
|
top = triggered[:3]
|
|
56
53
|
return ReasonedVerdict(
|
|
57
54
|
"confirm", f"发现 {len(triggered)} 个高遗忘风险知识点,建议优先复习:"
|