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
package/python/foxclaw/cli.py
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
foxclaw serve # 启动 API 服务(默认 http://127.0.0.1:8000)
|
|
8
8
|
foxclaw config check # 检查 .env / 环境变量:类型写错、拼错的键、互相矛盾的组合
|
|
9
9
|
foxclaw config show # 打印当前生效配置(密钥默认脱敏)
|
|
10
|
+
foxclaw skills install @owner/slug # 装 SKILL.md 技能(ClawHub/GitHub/URL/本地包)
|
|
11
|
+
foxclaw skills list # 列出已装技能
|
|
10
12
|
foxclaw --version # 打印版本
|
|
11
13
|
|
|
12
14
|
`serve` 用字符串目标 `foxclaw.api:app` 交给 uvicorn 导入,避免 CLI 进程
|
|
@@ -34,8 +36,6 @@ def _repo_root() -> Path:
|
|
|
34
36
|
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
35
37
|
from foxclaw.core import settings
|
|
36
38
|
|
|
37
|
-
# 启动前先查配置:类型写错的值今天会崩在很远的地方(uvicorn 40 行 traceback 中间的
|
|
38
|
-
# `int()`),用户看不出是自己 .env 写错了。这里提前拦下并说清怎么改。
|
|
39
39
|
issues = settings.check()
|
|
40
40
|
fatal = settings.fatal_issues(issues)
|
|
41
41
|
if fatal:
|
|
@@ -43,22 +43,19 @@ def _cmd_serve(args: argparse.Namespace) -> int:
|
|
|
43
43
|
print("\n以上配置会让启动直接失败,已中止。改好后重试。", file=sys.stderr)
|
|
44
44
|
return 2
|
|
45
45
|
if issues:
|
|
46
|
-
# 不影响启动的问题(可疑但能跑)只提醒一次,不拦
|
|
47
46
|
print(settings.format_issues(issues, limit=8), file=sys.stderr)
|
|
48
47
|
print("(以上不阻断启动;`foxclaw config check` 看完整诊断)", file=sys.stderr)
|
|
49
48
|
|
|
50
49
|
import uvicorn
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
kwargs["reload_dirs"] = [str(_repo_root() / "foxclaw")]
|
|
61
|
-
uvicorn.run(APP_TARGET, **kwargs)
|
|
51
|
+
uvicorn.run(
|
|
52
|
+
APP_TARGET,
|
|
53
|
+
host=args.host,
|
|
54
|
+
port=args.port,
|
|
55
|
+
log_level=args.log_level,
|
|
56
|
+
reload=bool(args.reload),
|
|
57
|
+
reload_dirs=[str(_repo_root() / "foxclaw")] if args.reload else None,
|
|
58
|
+
)
|
|
62
59
|
return 0
|
|
63
60
|
|
|
64
61
|
|
|
@@ -97,6 +94,12 @@ def _cmd_config_show(args: argparse.Namespace) -> int:
|
|
|
97
94
|
return 0
|
|
98
95
|
|
|
99
96
|
|
|
97
|
+
def _cmd_skills(args: argparse.Namespace) -> int:
|
|
98
|
+
from foxclaw.skills.install_cmd import run
|
|
99
|
+
|
|
100
|
+
return run(args)
|
|
101
|
+
|
|
102
|
+
|
|
100
103
|
def build_parser() -> argparse.ArgumentParser:
|
|
101
104
|
parser = argparse.ArgumentParser(
|
|
102
105
|
prog="foxclaw",
|
|
@@ -131,6 +134,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
131
134
|
help="显示密钥原文(默认脱敏;别在截图/日志里用)")
|
|
132
135
|
c_show.set_defaults(handler=_cmd_config_show)
|
|
133
136
|
|
|
137
|
+
skills = subparsers.add_parser("skills", help="安装/查看/卸载指令技能(SKILL.md)")
|
|
138
|
+
skills_sub = skills.add_subparsers(dest="skills_action", metavar="<action>")
|
|
139
|
+
|
|
140
|
+
s_install = skills_sub.add_parser("install", help="从 ClawHub/GitHub/URL/本地包装一个技能")
|
|
141
|
+
s_install.add_argument(
|
|
142
|
+
"ref",
|
|
143
|
+
help="@owner/slug(ClawHub) | github:owner/repo[/子路径][@ref] | "
|
|
144
|
+
"http(s)://包地址 | 本地 .zip/.tar.gz/SKILL.md/目录")
|
|
145
|
+
s_install.add_argument("--yes", action="store_true",
|
|
146
|
+
help="跳过确认(脚本/CI 必填;非终端环境没有它一定失败)")
|
|
147
|
+
s_install.add_argument("--id", default="", help="覆盖技能 id(默认取 SKILL.md 里的 name)")
|
|
148
|
+
s_install.add_argument("--json", action="store_true", help="输出 JSON")
|
|
149
|
+
s_install.set_defaults(handler=_cmd_skills)
|
|
150
|
+
|
|
151
|
+
s_list = skills_sub.add_parser("list", help="列出已安装的指令技能")
|
|
152
|
+
s_list.add_argument("--json", action="store_true", help="输出 JSON")
|
|
153
|
+
s_list.set_defaults(handler=_cmd_skills)
|
|
154
|
+
|
|
155
|
+
s_remove = skills_sub.add_parser("remove", help="卸载一个指令技能")
|
|
156
|
+
s_remove.add_argument("id", help="技能 id(`foxclaw skills list` 第一列)")
|
|
157
|
+
s_remove.set_defaults(handler=_cmd_skills)
|
|
158
|
+
|
|
134
159
|
return parser
|
|
135
160
|
|
|
136
161
|
|
|
@@ -143,6 +168,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
143
168
|
if getattr(args, "command", None) == "config":
|
|
144
169
|
print("用法:`foxclaw config check` 或 `foxclaw config show`")
|
|
145
170
|
return 2
|
|
171
|
+
if getattr(args, "command", None) == "skills":
|
|
172
|
+
print("用法:`foxclaw skills install <ref>` / `foxclaw skills list` / "
|
|
173
|
+
"`foxclaw skills remove <id>`")
|
|
174
|
+
return 2
|
|
146
175
|
print(f"foxclaw {__version__}")
|
|
147
176
|
print("用 `foxclaw serve` 启动本地服务;`foxclaw config check` 检查配置;"
|
|
148
177
|
"`foxclaw --help` 查看全部子命令。")
|
|
@@ -15,10 +15,10 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class Evidence:
|
|
18
|
-
factor: str
|
|
19
|
-
detail: str = ""
|
|
20
|
-
weight: float = 1.0
|
|
21
|
-
raw: Any = None
|
|
18
|
+
factor: str
|
|
19
|
+
detail: str = ""
|
|
20
|
+
weight: float = 1.0
|
|
21
|
+
raw: Any = None
|
|
22
22
|
|
|
23
23
|
def to_dict(self) -> dict:
|
|
24
24
|
return {"factor": self.factor, "detail": self.detail, "weight": self.weight}
|
|
@@ -26,11 +26,11 @@ class Evidence:
|
|
|
26
26
|
|
|
27
27
|
@dataclass
|
|
28
28
|
class ReasonedVerdict:
|
|
29
|
-
decision: str
|
|
30
|
-
rationale: str
|
|
31
|
-
evidence: list = field(default_factory=list)
|
|
32
|
-
hypotheses: list = field(default_factory=list)
|
|
33
|
-
source: str = "rules"
|
|
29
|
+
decision: str
|
|
30
|
+
rationale: str
|
|
31
|
+
evidence: list = field(default_factory=list)
|
|
32
|
+
hypotheses: list = field(default_factory=list)
|
|
33
|
+
source: str = "rules"
|
|
34
34
|
|
|
35
35
|
def to_dict(self) -> dict:
|
|
36
36
|
return {"decision": self.decision, "rationale": self.rationale,
|
|
@@ -61,7 +61,6 @@ class DomainReasoner:
|
|
|
61
61
|
ev = self.gather_evidence(subject, obs)
|
|
62
62
|
return self.arbitrate(subject, obs, hyps, ev)
|
|
63
63
|
|
|
64
|
-
# ── 双层增强的通用工具(v2 ③ Step4:itinerary/learning 复用)────────────
|
|
65
64
|
@staticmethod
|
|
66
65
|
def llm_available(llm, enabled: bool) -> bool:
|
|
67
66
|
"""LLM 是否真正可用:启用 + 非空 + 非 stub(无真实模型时视为不可用)。"""
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
可驱动前端"系统在想什么"实时面板,也用于调试与回放。
|
|
11
11
|
"""
|
|
12
12
|
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
13
15
|
import time
|
|
14
16
|
from dataclasses import dataclass, field
|
|
15
17
|
|
|
@@ -18,19 +20,18 @@ from dataclasses import dataclass, field
|
|
|
18
20
|
class CognitiveState:
|
|
19
21
|
"""系统当前认知状态快照。随事件总线上的活动自动更新。"""
|
|
20
22
|
|
|
21
|
-
active_goal: str = None
|
|
22
|
-
active_agent: str = None
|
|
23
|
-
semantic_focus: str = None
|
|
24
|
-
attention_scope: list = field(default_factory=list)
|
|
25
|
-
reasoning_depth: float = 0.0
|
|
26
|
-
confidence_map: dict = field(default_factory=dict)
|
|
27
|
-
activated_memories: list = field(default_factory=list)
|
|
28
|
-
last_route: str = None
|
|
29
|
-
active_processes: int = 0
|
|
30
|
-
temporal_window: str = "current_turn"
|
|
23
|
+
active_goal: Optional[str] = None
|
|
24
|
+
active_agent: Optional[str] = None
|
|
25
|
+
semantic_focus: Optional[str] = None
|
|
26
|
+
attention_scope: list = field(default_factory=list)
|
|
27
|
+
reasoning_depth: float = 0.0
|
|
28
|
+
confidence_map: dict = field(default_factory=dict)
|
|
29
|
+
activated_memories: list = field(default_factory=list)
|
|
30
|
+
last_route: Optional[str] = None
|
|
31
|
+
active_processes: int = 0
|
|
32
|
+
temporal_window: str = "current_turn"
|
|
31
33
|
updated_at: float = field(default_factory=time.time)
|
|
32
34
|
|
|
33
|
-
# ── 信号摄入(由事件驱动)──
|
|
34
35
|
|
|
35
36
|
def on_execution(self, payload: dict):
|
|
36
37
|
"""执行事件 → 更新目标/Agent/注意力。"""
|
|
@@ -54,13 +55,12 @@ class CognitiveState:
|
|
|
54
55
|
content = mu.get("content")
|
|
55
56
|
if content:
|
|
56
57
|
snippet = content[:80]
|
|
57
|
-
# 去重:已存在则先移除旧的,再插到最前(LRU 式)
|
|
58
58
|
existing = [m for m in self.activated_memories if m != snippet]
|
|
59
59
|
self.activated_memories = [snippet] + existing[:4]
|
|
60
60
|
self.temporal_window = "memory_augmented"
|
|
61
61
|
self.updated_at = time.time()
|
|
62
62
|
|
|
63
|
-
def on_process_change(self, payload: dict, active_count: int = None):
|
|
63
|
+
def on_process_change(self, payload: dict, active_count: int | None = None):
|
|
64
64
|
"""进程状态变更 → 更新活跃进程数。"""
|
|
65
65
|
if active_count is not None:
|
|
66
66
|
self.active_processes = active_count
|
|
@@ -69,14 +69,12 @@ class CognitiveState:
|
|
|
69
69
|
@staticmethod
|
|
70
70
|
def _keywords(text: str, n: int = 6) -> list:
|
|
71
71
|
import re
|
|
72
|
-
# 先按标点/空格切;若切出的片段仍很长(中文无空格),按 2-4 字滑窗取要点
|
|
73
72
|
rough = [w for w in re.split(r"[\s,,、。!?:;]", text or "") if len(w) >= 2]
|
|
74
73
|
out = []
|
|
75
74
|
for seg in rough:
|
|
76
75
|
if len(seg) <= 4:
|
|
77
76
|
out.append(seg)
|
|
78
77
|
else:
|
|
79
|
-
# 长片段取首尾 + 中部双字,作为注意力要点
|
|
80
78
|
out.append(seg[:3])
|
|
81
79
|
if len(seg) > 6:
|
|
82
80
|
out.append(seg[len(seg)//2:len(seg)//2+2])
|
|
@@ -106,25 +104,23 @@ class CognitiveStateTracker:
|
|
|
106
104
|
import threading
|
|
107
105
|
self.state = CognitiveState()
|
|
108
106
|
self.process_manager = process_manager
|
|
109
|
-
self._lock = threading.Lock()
|
|
107
|
+
self._lock = threading.Lock()
|
|
110
108
|
if bus is not None:
|
|
111
|
-
# 【P4-1】前两个是精确 channel 名 → exact=True;task.lifecycle 是刻意的前缀
|
|
112
|
-
# 订阅(收 created/queued/running/... 一族),保持默认前缀匹配。
|
|
113
109
|
bus.subscribe("execution.completed", self._on_exec, exact=True)
|
|
114
110
|
bus.subscribe("memory.updated", self._on_mem, exact=True)
|
|
115
|
-
bus.subscribe("task.lifecycle", self._on_proc)
|
|
111
|
+
bus.subscribe("task.lifecycle", self._on_proc)
|
|
116
112
|
|
|
117
113
|
def _on_exec(self, msg):
|
|
118
114
|
if msg is None:
|
|
119
115
|
return
|
|
120
116
|
with self._lock:
|
|
121
|
-
self.state.on_execution(getattr(msg, "payload", None))
|
|
117
|
+
self.state.on_execution(getattr(msg, "payload", None) or {})
|
|
122
118
|
|
|
123
119
|
def _on_mem(self, msg):
|
|
124
120
|
if msg is None:
|
|
125
121
|
return
|
|
126
122
|
with self._lock:
|
|
127
|
-
self.state.on_memory_updated(getattr(msg, "payload", None))
|
|
123
|
+
self.state.on_memory_updated(getattr(msg, "payload", None) or {})
|
|
128
124
|
|
|
129
125
|
def _on_proc(self, msg):
|
|
130
126
|
if msg is None:
|
|
@@ -132,15 +128,13 @@ class CognitiveStateTracker:
|
|
|
132
128
|
active = None
|
|
133
129
|
pm = self.process_manager
|
|
134
130
|
if pm is not None:
|
|
135
|
-
# 【深审优化】用 pm 的 O(1) active_count(),免去每次 lifecycle 事件 stats() 遍历
|
|
136
|
-
# 全部进程(O(n))。旧 pm 无此方法则回退到 stats()(scheduled+running)。
|
|
137
131
|
if hasattr(pm, "active_count"):
|
|
138
132
|
active = pm.active_count()
|
|
139
133
|
else:
|
|
140
134
|
st = pm.stats().get("by_state", {})
|
|
141
135
|
active = st.get("scheduled", 0) + st.get("running", 0)
|
|
142
136
|
with self._lock:
|
|
143
|
-
self.state.on_process_change(getattr(msg, "payload", None), active)
|
|
137
|
+
self.state.on_process_change(getattr(msg, "payload", None) or {}, active)
|
|
144
138
|
|
|
145
139
|
def snapshot(self) -> dict:
|
|
146
140
|
with self._lock:
|
|
@@ -28,10 +28,8 @@ import hashlib
|
|
|
28
28
|
import json
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
|
|
31
|
-
#: 首条记录的 prev_hash。用 64 个 0 而不是空串:一眼能看出「这是链头」。
|
|
32
31
|
GENESIS = "0" * 64
|
|
33
32
|
|
|
34
|
-
#: 从文件尾部回读的字节数:找 tail hash 只需最后一行,64KB 足够容下超长记录。
|
|
35
33
|
_TAIL_WINDOW = 65536
|
|
36
34
|
|
|
37
35
|
|
|
@@ -100,7 +98,7 @@ def verify(path) -> tuple:
|
|
|
100
98
|
return False, n, f"第 {i} 行不是合法 JSON"
|
|
101
99
|
h = rec.get("hash")
|
|
102
100
|
if not h:
|
|
103
|
-
continue
|
|
101
|
+
continue
|
|
104
102
|
if rec.get("prev_hash") != prev:
|
|
105
103
|
return False, n, (f"第 {i} 行链断:prev_hash 与上一条 hash 不符"
|
|
106
104
|
"(中间记录被删或被挪动)")
|
|
@@ -50,10 +50,10 @@ class SharedWriteAuditSink:
|
|
|
50
50
|
def __init__(self, path: Path):
|
|
51
51
|
self.path = Path(path)
|
|
52
52
|
self._lock = threading.Lock()
|
|
53
|
-
self._last_hash: str | None = None
|
|
53
|
+
self._last_hash: str | None = None
|
|
54
54
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
55
|
|
|
56
|
-
def __call__(self, evt):
|
|
56
|
+
def __call__(self, evt):
|
|
57
57
|
try:
|
|
58
58
|
rec = {
|
|
59
59
|
"ts": datetime.now().isoformat(timespec="seconds"),
|
|
@@ -69,7 +69,7 @@ class SharedWriteAuditSink:
|
|
|
69
69
|
f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
|
|
70
70
|
self._last_hash = rec["hash"]
|
|
71
71
|
except Exception:
|
|
72
|
-
pass
|
|
72
|
+
pass
|
|
73
73
|
|
|
74
74
|
def read(self, limit: int = 200) -> list:
|
|
75
75
|
"""读最近 limit 条(供诊断/工具用)。坏行跳过 —— 落盘绝不该反过来卡死读取。"""
|
|
@@ -25,7 +25,6 @@ import re
|
|
|
25
25
|
from dataclasses import dataclass, field
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
# 粗略 token 估算:中文约 1 字符 ≈ 1 token,英文约 4 字符 ≈ 1 token。
|
|
29
28
|
def estimate_tokens(text: str) -> int:
|
|
30
29
|
if not text:
|
|
31
30
|
return 0
|
|
@@ -38,8 +37,6 @@ def estimate_messages_tokens(messages: list) -> int:
|
|
|
38
37
|
return sum(estimate_tokens(str(m.get("content", ""))) for m in messages)
|
|
39
38
|
|
|
40
39
|
|
|
41
|
-
# 关键标识符:不能在摘要中丢失的(ID、长数字、订单号、哈希、金额等)
|
|
42
|
-
# 不用外层 \b:¥/逗号不是单词字符,会导致金额匹配失败。
|
|
43
40
|
_IDENTIFIER_PAT = re.compile(
|
|
44
41
|
r"[a-zA-Z]{2,}_[a-zA-Z0-9]{4,}|[a-f0-9]{8,}|[0-9]{6,}|¥[\d,]+(?:\.\d+)?")
|
|
45
42
|
|
|
@@ -56,14 +53,14 @@ def extract_identifiers(messages: list) -> list[str]:
|
|
|
56
53
|
|
|
57
54
|
@dataclass
|
|
58
55
|
class CompactionResult:
|
|
59
|
-
messages: list = field(default_factory=list)
|
|
60
|
-
method: str = "none"
|
|
56
|
+
messages: list = field(default_factory=list)
|
|
57
|
+
method: str = "none"
|
|
61
58
|
original_count: int = 0
|
|
62
59
|
compacted_count: int = 0
|
|
63
60
|
original_tokens: int = 0
|
|
64
61
|
compacted_tokens: int = 0
|
|
65
62
|
preserved_identifiers: list = field(default_factory=list)
|
|
66
|
-
note: str = ""
|
|
63
|
+
note: str = ""
|
|
67
64
|
|
|
68
65
|
def to_dict(self) -> dict:
|
|
69
66
|
return {
|
|
@@ -101,8 +98,6 @@ def group_tool_calls(messages: list) -> list:
|
|
|
101
98
|
m = messages[i]
|
|
102
99
|
buf.append(m)
|
|
103
100
|
if m.get("role") == "assistant" and (m.get("tool_calls") or []):
|
|
104
|
-
# 本 assistant 声明了工具调用 → 把紧随其后、id 匹配的 tool 结果
|
|
105
|
-
# 全部并入同一组,确保「声明与结果同进同出」。
|
|
106
101
|
ids = {tc.get("id") for tc in (m.get("tool_calls") or [])}
|
|
107
102
|
j = i + 1
|
|
108
103
|
while j < n:
|
|
@@ -125,10 +120,10 @@ class Compactor:
|
|
|
125
120
|
|
|
126
121
|
def __init__(self, router=None, max_tokens: int = 6000,
|
|
127
122
|
keep_recent: int = 4, oversized_token: int = 2000):
|
|
128
|
-
self.router = router
|
|
129
|
-
self.max_tokens = max_tokens
|
|
130
|
-
self.keep_recent = keep_recent
|
|
131
|
-
self.oversized_token = oversized_token
|
|
123
|
+
self.router = router
|
|
124
|
+
self.max_tokens = max_tokens
|
|
125
|
+
self.keep_recent = keep_recent
|
|
126
|
+
self.oversized_token = oversized_token
|
|
132
127
|
|
|
133
128
|
def needs_compaction(self, messages: list) -> bool:
|
|
134
129
|
return estimate_messages_tokens(messages) > self.max_tokens
|
|
@@ -146,13 +141,9 @@ class Compactor:
|
|
|
146
141
|
res.compacted_tokens = res.original_tokens
|
|
147
142
|
return res
|
|
148
143
|
|
|
149
|
-
# 分区:系统消息 / 待压缩的早期消息 / 保留的近期消息
|
|
150
144
|
system_msgs = [m for m in messages if m.get("role") == "system"]
|
|
151
145
|
non_system = [m for m in messages if m.get("role") != "system"]
|
|
152
146
|
keep = max(0, self.keep_recent)
|
|
153
|
-
# 按**原子组**保留(而非按条数),保证 assistant.tool_calls 与其 tool 结果
|
|
154
|
-
# 同进同出。按条数切会在「一轮多工具」时把 tool 结果与其声明切散 →
|
|
155
|
-
# provider 报 400。详见 group_tool_calls docstring。
|
|
156
147
|
_groups = group_tool_calls(non_system)
|
|
157
148
|
if keep and _groups:
|
|
158
149
|
recent = [m for g in _groups[-keep:] for m in g]
|
|
@@ -161,8 +152,6 @@ class Compactor:
|
|
|
161
152
|
recent, to_compress = [], non_system
|
|
162
153
|
else:
|
|
163
154
|
recent, to_compress = [], non_system
|
|
164
|
-
# 防御:若仍不配对(极端边界/上游已产生的畸形序列),宁可放弃压缩,
|
|
165
|
-
# 也不产出非法消息序列 —— 压缩不生效只是浪费 token,发出去 400 是整轮失败。
|
|
166
155
|
_declared = {tc.get("id") for m in (recent + to_compress)
|
|
167
156
|
for tc in (m.get("tool_calls") or [])}
|
|
168
157
|
_orphans = [m for m in (recent + to_compress)
|
|
@@ -185,15 +174,12 @@ class Compactor:
|
|
|
185
174
|
|
|
186
175
|
res.preserved_identifiers = extract_identifiers(to_compress)
|
|
187
176
|
|
|
188
|
-
# —— 渐进降级 ——
|
|
189
177
|
summary = None
|
|
190
178
|
method = "truncate"
|
|
191
|
-
# 1. LLM 完整摘要
|
|
192
179
|
if self.router is not None:
|
|
193
180
|
summary = self._try_summarize(to_compress, res.preserved_identifiers)
|
|
194
181
|
if summary:
|
|
195
182
|
method = "llm_summary"
|
|
196
|
-
# 2. 部分摘要:排除超大消息后再试
|
|
197
183
|
if summary is None and self.router is not None:
|
|
198
184
|
trimmed = [m for m in to_compress
|
|
199
185
|
if estimate_tokens(str(m.get("content", ""))) <= self.oversized_token]
|
|
@@ -201,7 +187,6 @@ class Compactor:
|
|
|
201
187
|
summary = self._try_summarize(trimmed, res.preserved_identifiers)
|
|
202
188
|
if summary:
|
|
203
189
|
method = "partial_summary"
|
|
204
|
-
# 3. 截断兜底
|
|
205
190
|
if summary is None:
|
|
206
191
|
summary = self._truncate_fallback(to_compress, res.preserved_identifiers)
|
|
207
192
|
method = "truncate"
|
|
@@ -211,14 +196,11 @@ class Compactor:
|
|
|
211
196
|
compacted = system_msgs + [summary_msg] + recent
|
|
212
197
|
compacted_tokens = estimate_messages_tokens(compacted)
|
|
213
198
|
|
|
214
|
-
# 校验:压缩须真的显著变小(至少省 10%),否则放弃——避免系统消息过大时
|
|
215
|
-
# 压缩无效还白加摘要导致反而更大。
|
|
216
199
|
if compacted_tokens >= res.original_tokens * 0.9:
|
|
217
200
|
res.messages = list(messages)
|
|
218
201
|
res.method = "skipped_ineffective"
|
|
219
202
|
res.compacted_count = len(messages)
|
|
220
203
|
res.compacted_tokens = res.original_tokens
|
|
221
|
-
# 提示:压缩无效通常因系统消息/近期消息本身已占满预算
|
|
222
204
|
sys_tokens = estimate_messages_tokens(system_msgs)
|
|
223
205
|
res.note = (f"压缩跳过:有效压缩空间不足(系统消息占 {sys_tokens} token)。"
|
|
224
206
|
f"建议精简系统提示或调大上下文预算。")
|
|
@@ -242,7 +224,7 @@ class Compactor:
|
|
|
242
224
|
return list(messages), None
|
|
243
225
|
res = self.compact(messages)
|
|
244
226
|
if res.method == "none" or res.method.startswith("skipped"):
|
|
245
|
-
return list(messages), res
|
|
227
|
+
return list(messages), res
|
|
246
228
|
return res.messages, res
|
|
247
229
|
except Exception:
|
|
248
230
|
return list(messages), None
|
|
@@ -22,7 +22,7 @@ class LayeredConfig:
|
|
|
22
22
|
"""三层配置合并原语。持有各层 dict 的引用 —— 若某层的集合值是可变对象(如 SkillPolicy 的
|
|
23
23
|
user deny set),原地增删会被 merged_set/policy_locked 实时反映,无需重建。"""
|
|
24
24
|
|
|
25
|
-
def __init__(self, policy: dict = None, user: dict = None, runtime: dict = None):
|
|
25
|
+
def __init__(self, policy: dict | None = None, user: dict | None = None, runtime: dict | None = None):
|
|
26
26
|
self.policy = dict(policy or {})
|
|
27
27
|
self.user = dict(user or {})
|
|
28
28
|
self.runtime = dict(runtime or {})
|
|
@@ -61,5 +61,5 @@ def load_policy_layer(data_dir=None) -> dict:
|
|
|
61
61
|
d = json.loads(p.read_text(encoding="utf-8"))
|
|
62
62
|
deny |= set(d.get("deny") or [])
|
|
63
63
|
except Exception:
|
|
64
|
-
pass
|
|
64
|
+
pass
|
|
65
65
|
return {"deny": sorted(deny)}
|
|
@@ -25,10 +25,9 @@ import time
|
|
|
25
25
|
from dataclasses import dataclass, field
|
|
26
26
|
from typing import Optional, Tuple
|
|
27
27
|
|
|
28
|
-
#: 视为「关键操作」的副作用级别(发送/支付);destructive 无条件算关键
|
|
29
28
|
CRITICAL_SIDE_EFFECTS = ("send", "pay")
|
|
30
29
|
|
|
31
|
-
DEFAULT_TTL = 300.0
|
|
30
|
+
DEFAULT_TTL = 300.0
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
def action_fingerprint(action: str, args: dict) -> str:
|
|
@@ -78,14 +77,13 @@ class ConfirmationBroker:
|
|
|
78
77
|
self._items: dict[str, PendingConfirm] = {}
|
|
79
78
|
self._lock = threading.Lock()
|
|
80
79
|
|
|
81
|
-
# ── 签发 ────────────────────────────────────────────
|
|
82
80
|
def issue(self, session: str, action: str, args: dict,
|
|
83
|
-
ttl: float = None) -> dict:
|
|
81
|
+
ttl: float | None = None) -> dict:
|
|
84
82
|
"""签发一个绑定到 (会话, 动作, 参数) 的一次性确认码。"""
|
|
85
83
|
now = self._clock()
|
|
86
84
|
ttl = self.ttl if ttl is None else float(ttl)
|
|
87
85
|
item = PendingConfirm(
|
|
88
|
-
nonce=secrets.token_hex(8),
|
|
86
|
+
nonce=secrets.token_hex(8),
|
|
89
87
|
action=action or "",
|
|
90
88
|
fingerprint=action_fingerprint(action or "", args or {}),
|
|
91
89
|
session=session or "",
|
|
@@ -99,9 +97,8 @@ class ConfirmationBroker:
|
|
|
99
97
|
return {"nonce": item.nonce, "action": item.action,
|
|
100
98
|
"expires_at": item.expires, "ttl": ttl}
|
|
101
99
|
|
|
102
|
-
# ── 校验并消费 ──────────────────────────────────────
|
|
103
100
|
def consume(self, session: str, nonce: str, action: str = "",
|
|
104
|
-
args: dict = None, now: float = None) -> Tuple[bool, str]:
|
|
101
|
+
args: dict | None = None, now: float | None = None) -> Tuple[bool, str]:
|
|
105
102
|
"""校验确认码并**立即作废**。返回 (是否通过, 原因码)。
|
|
106
103
|
|
|
107
104
|
原因码:``ok`` / ``missing_nonce`` / ``unknown_nonce`` / ``already_used`` /
|
|
@@ -113,8 +110,6 @@ class ConfirmationBroker:
|
|
|
113
110
|
now = now if now is not None else self._clock()
|
|
114
111
|
with self._lock:
|
|
115
112
|
item: Optional[PendingConfirm] = self._items.get(nonce)
|
|
116
|
-
# 先看这一条再清理:否则过期的码会被 _purge 抹掉,排错时只会看到
|
|
117
|
-
# unknown_nonce,分不清"从没发过"和"发过但过期了"
|
|
118
113
|
if item is not None and item.expires <= now:
|
|
119
114
|
self._items.pop(nonce, None)
|
|
120
115
|
return False, "expired"
|
|
@@ -130,10 +125,9 @@ class ConfirmationBroker:
|
|
|
130
125
|
if args is not None and item.fingerprint != action_fingerprint(item.action, args):
|
|
131
126
|
return False, "args_mismatch"
|
|
132
127
|
item.used = True
|
|
133
|
-
self._items.pop(nonce, None)
|
|
128
|
+
self._items.pop(nonce, None)
|
|
134
129
|
return True, "ok"
|
|
135
130
|
|
|
136
|
-
# ── 观测 ────────────────────────────────────────────
|
|
137
131
|
def pending(self, session: str = "") -> list:
|
|
138
132
|
"""未消费且未过期的确认码(可按会话过滤),供排查/前端展示。"""
|
|
139
133
|
now = self._clock()
|
|
@@ -142,7 +136,7 @@ class ConfirmationBroker:
|
|
|
142
136
|
return [it.to_dict() for it in self._items.values()
|
|
143
137
|
if not it.used and (not session or it.session == session)]
|
|
144
138
|
|
|
145
|
-
def purge(self, now: float = None) -> int:
|
|
139
|
+
def purge(self, now: float | None = None) -> int:
|
|
146
140
|
now = now if now is not None else self._clock()
|
|
147
141
|
with self._lock:
|
|
148
142
|
return self._purge_locked(now)
|