foxclaw 0.1.0 → 0.1.2
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/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +152 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +148 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1637 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +46 -0
- package/python/pyproject.toml +265 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
用 FastAPI TestClient 在同进程内验证 API 端点。
|
|
3
|
+
绕过沙箱对后台进程/loopback 的限制,直接走 ASGI。
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
import json
|
|
12
|
+
from fastapi.testclient import TestClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def show(title, data, limit=None):
|
|
16
|
+
print(f"\n【{title}】")
|
|
17
|
+
s = json.dumps(data, ensure_ascii=False, indent=2)
|
|
18
|
+
if limit:
|
|
19
|
+
s = "\n".join(s.splitlines()[:limit])
|
|
20
|
+
print(s)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_api():
|
|
24
|
+
# 环境变量须在 import foxclaw.api 前设好(模块加载时按环境变量建网关/agent)
|
|
25
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
26
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_") # 不污染仓库根
|
|
27
|
+
from foxclaw.api import app
|
|
28
|
+
|
|
29
|
+
with TestClient(app) as c:
|
|
30
|
+
print("="*64)
|
|
31
|
+
print("FOXCLAW · API 端到端验证 (ASGI TestClient)")
|
|
32
|
+
print("="*64)
|
|
33
|
+
|
|
34
|
+
# 1. 系统状态
|
|
35
|
+
r = c.get("/api/status")
|
|
36
|
+
print(f"\n[GET /api/status] HTTP {r.status_code}")
|
|
37
|
+
data = r.json()
|
|
38
|
+
for aid, s in data.items():
|
|
39
|
+
tc = s["thought_chain"]
|
|
40
|
+
print(f" {aid:<12} 节点={tc['total_nodes']:<3} shadow={s['harness']['shadow_mode']}")
|
|
41
|
+
|
|
42
|
+
# 2. 发任务(真实经过 ThoughtChain + Harness)
|
|
43
|
+
print("\n[POST /api/task] 连发 7 个财务任务触发校准")
|
|
44
|
+
for i, t in enumerate(["分析支出","制定预算","评估风险","季度报告","追踪消费","明年规划","同期对比"], 1):
|
|
45
|
+
r = c.post("/api/task", json={"task": t, "agent_id": "finance"})
|
|
46
|
+
d = r.json()
|
|
47
|
+
err = d.get("error", "ok")
|
|
48
|
+
print(f" [{i}] {t} → HTTP {r.status_code} · {err}")
|
|
49
|
+
|
|
50
|
+
# 3. Harness 状态(校准后)
|
|
51
|
+
r = c.get("/api/harness/finance/status")
|
|
52
|
+
show("GET /api/harness/finance/status", r.json())
|
|
53
|
+
|
|
54
|
+
# 4. 推理链
|
|
55
|
+
r = c.get("/api/thoughtchain/finance/chain")
|
|
56
|
+
nodes = r.json()["nodes"]
|
|
57
|
+
print(f"\n[GET /api/thoughtchain/finance/chain] HTTP {r.status_code} · {len(nodes)} 节点")
|
|
58
|
+
for n in nodes[-5:]:
|
|
59
|
+
print(f" {n['type']:<16} {n['harness_status']:<9} "
|
|
60
|
+
f"conf={n['conf']:.2f} Δ={n.get('harness_delta') or 0:.3f}")
|
|
61
|
+
|
|
62
|
+
# 5. 漂移趋势
|
|
63
|
+
r = c.get("/api/thoughtchain/finance/trend?limit=10")
|
|
64
|
+
trend = r.json()["trend"]
|
|
65
|
+
print(f"\n[GET .../trend] {len(trend)} 数据点 · Δ 序列:")
|
|
66
|
+
print(" " + " ".join(f"{t['delta']:.2f}" for t in trend[:10]))
|
|
67
|
+
|
|
68
|
+
# 6. Policy
|
|
69
|
+
r = c.get("/api/harness/finance/policy")
|
|
70
|
+
pol = r.json()
|
|
71
|
+
print(f"\n[GET /api/harness/finance/policy] v{pol['version']} · "
|
|
72
|
+
f"{len(pol.get('rules',[]))} 规则")
|
|
73
|
+
|
|
74
|
+
# 7. 偏好管理端点(F3):GET 列出结构;DELETE 不存在 id 优雅返回 ok=False(不崩)
|
|
75
|
+
r = c.get("/api/preferences")
|
|
76
|
+
assert r.status_code == 200 and "preferences" in r.json(), r.text
|
|
77
|
+
r = c.delete("/api/preferences/nonexistent_id")
|
|
78
|
+
assert r.status_code == 200 and r.json().get("ok") is False, r.text
|
|
79
|
+
print("\n[GET/DELETE /api/preferences] 列出+删除通路 ok")
|
|
80
|
+
|
|
81
|
+
print("\n" + "="*64)
|
|
82
|
+
print("✓ API 端到端验证完成 · 所有端点返回真实数据")
|
|
83
|
+
print("="*64)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
test_api()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API 可选鉴权回归
|
|
3
|
+
|
|
4
|
+
验证 FOXCLAW_API_KEY:
|
|
5
|
+
· 未设 → /api/* 开放(本地默认,既有行为不变);
|
|
6
|
+
· 设了 → /api/* 与 /ws/* 需带正确 X-API-Key,否则 401;前端与 /docs 始终开放。
|
|
7
|
+
中间件每请求读环境变量,故可在同一 TestClient 内切换验证。
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
13
|
+
# 本测试要精确控制 FOXCLAW_API_KEY 的有/无;若 .env 带真实 key 会污染判定
|
|
14
|
+
# (见 foxclaw/api.py 中 FOXCLAW_SKIP_DOTENV 的说明),故关掉 .env。
|
|
15
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
16
|
+
|
|
17
|
+
import tempfile
|
|
18
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_") # 不污染仓库根
|
|
19
|
+
from fastapi.testclient import TestClient
|
|
20
|
+
from foxclaw.api import app
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check(cond, msg):
|
|
24
|
+
if not cond:
|
|
25
|
+
raise AssertionError(f"✗ {msg}")
|
|
26
|
+
print(f" ✓ {msg}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
print("=" * 56)
|
|
31
|
+
print("API 可选鉴权回归")
|
|
32
|
+
print("=" * 56)
|
|
33
|
+
os.environ.pop("FOXCLAW_API_KEY", None)
|
|
34
|
+
with TestClient(app) as c:
|
|
35
|
+
print("\n【1】未设 FOXCLAW_API_KEY:开放(本地默认)")
|
|
36
|
+
check(c.get("/api/status").status_code == 200, "/api/status 开放(200)")
|
|
37
|
+
|
|
38
|
+
print("\n【2】设了 FOXCLAW_API_KEY:/api/* 受保护")
|
|
39
|
+
os.environ["FOXCLAW_API_KEY"] = "secret-123"
|
|
40
|
+
check(c.get("/api/status").status_code == 401, "无 X-API-Key → 401")
|
|
41
|
+
check(c.get("/api/status", headers={"X-API-Key": "wrong"}).status_code == 401,
|
|
42
|
+
"错误 key → 401")
|
|
43
|
+
check(c.get("/api/status", headers={"X-API-Key": "secret-123"}).status_code == 200,
|
|
44
|
+
"正确 key → 200")
|
|
45
|
+
|
|
46
|
+
print("\n【3】WebSocket 鉴权(端点内 ?key=,http 中间件管不到 ws)")
|
|
47
|
+
# 无 key → 握手被拒(端点 close 1008)
|
|
48
|
+
rejected = False
|
|
49
|
+
try:
|
|
50
|
+
with c.websocket_connect("/ws/harness"):
|
|
51
|
+
pass
|
|
52
|
+
except Exception:
|
|
53
|
+
rejected = True
|
|
54
|
+
check(rejected, "WS 无 key → 握手被拒")
|
|
55
|
+
# 带 ?key= → 连上
|
|
56
|
+
connected = False
|
|
57
|
+
try:
|
|
58
|
+
with c.websocket_connect("/ws/harness?key=secret-123"):
|
|
59
|
+
connected = True
|
|
60
|
+
except Exception:
|
|
61
|
+
connected = False
|
|
62
|
+
check(connected, "WS 带正确 ?key= → 连上")
|
|
63
|
+
|
|
64
|
+
print("\n【4】开鉴权时前端 / 文档仍开放")
|
|
65
|
+
check(c.get("/docs").status_code == 200, "/docs 无需 key(200)")
|
|
66
|
+
r_root = c.get("/")
|
|
67
|
+
check(r_root.status_code in (200, 404), f"/ 非 401(前端不被 key 挡, {r_root.status_code})")
|
|
68
|
+
|
|
69
|
+
print("\n【5】撤销 key → 恢复开放")
|
|
70
|
+
os.environ.pop("FOXCLAW_API_KEY", None)
|
|
71
|
+
check(c.get("/api/status").status_code == 200, "撤 key 后 /api/status 恢复开放")
|
|
72
|
+
|
|
73
|
+
print("\n" + "=" * 56)
|
|
74
|
+
print("API 鉴权回归通过")
|
|
75
|
+
print("=" * 56)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_api_auth():
|
|
79
|
+
main()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""API 层 Host 头校验(DNS rebinding 防护)。
|
|
2
|
+
|
|
3
|
+
攻击模型:本机 `foxclaw serve`(回环监听、无 API key)时,用户被诱导打开 evil.com;
|
|
4
|
+
evil.com 解析到 127.0.0.1,页面内 fetch 打本机 /api/*,同源成立 → 可读写本地数据面。
|
|
5
|
+
防线是校验 Host 头:浏览器发请求时 Host 必须与被访问域名一致,DNS 改解析也绕不过。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from foxclaw.api.middleware import _api_key_guard, _host_header_allowed
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.parametrize("host", [
|
|
19
|
+
"127.0.0.1:8000", "localhost:8000", "localhost", "TESTCLIENT",
|
|
20
|
+
"[::1]:8000", "192.168.1.9:8000", "10.0.0.2", "mypc", "",
|
|
21
|
+
])
|
|
22
|
+
def test_allows_local_and_lan_hosts(host: str) -> None:
|
|
23
|
+
assert _host_header_allowed(host) is True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("host", [
|
|
27
|
+
"evil.com", "attacker.example.org:8000", "sub.evil.com:80",
|
|
28
|
+
])
|
|
29
|
+
def test_rejects_public_domain_hosts(host: str) -> None:
|
|
30
|
+
assert _host_header_allowed(host) is False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_allowlist_escape_hatch(monkeypatch) -> None:
|
|
34
|
+
monkeypatch.setenv("FOXCLAW_ALLOWED_HOSTS", "my.host.net, *.corp.example")
|
|
35
|
+
assert _host_header_allowed("my.host.net") is True
|
|
36
|
+
assert _host_header_allowed("a.corp.example") is True
|
|
37
|
+
assert _host_header_allowed("evil.com") is False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _request(path: str, host: str, client: str = "127.0.0.1"):
|
|
41
|
+
return SimpleNamespace(
|
|
42
|
+
url=SimpleNamespace(path=path),
|
|
43
|
+
client=SimpleNamespace(host=client),
|
|
44
|
+
headers={"host": host, "X-API-Key": ""},
|
|
45
|
+
method="GET",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _ok(_request):
|
|
50
|
+
return SimpleNamespace(status_code=200)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize("path", ["/api/task", "/knowledge/ingest", "/config/llm"])
|
|
54
|
+
def test_loopback_with_public_host_is_forbidden(path: str, monkeypatch) -> None:
|
|
55
|
+
monkeypatch.delenv("FOXCLAW_API_KEY", raising=False)
|
|
56
|
+
resp = asyncio.run(_api_key_guard(_request(path, "evil.com"), _ok))
|
|
57
|
+
assert resp.status_code == 403, "本机客户端 + 公网域名 Host 必须拒绝"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_loopback_with_local_host_passes(monkeypatch) -> None:
|
|
61
|
+
monkeypatch.delenv("FOXCLAW_API_KEY", raising=False)
|
|
62
|
+
resp = asyncio.run(_api_key_guard(_request("/api/task", "127.0.0.1:8000"), _ok))
|
|
63
|
+
assert resp.status_code == 200
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_probe_paths_not_affected(monkeypatch) -> None:
|
|
67
|
+
monkeypatch.delenv("FOXCLAW_API_KEY", raising=False)
|
|
68
|
+
resp = asyncio.run(_api_key_guard(_request("/health", "evil.com"), _ok))
|
|
69
|
+
assert resp.status_code == 200, "探针不在数据面防护范围"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""聊天附件(多模态渲染·步骤7)回归测试:文件上传/下载、正文提取、护栏、LLM 上下文注入、会话落盘。
|
|
2
|
+
|
|
3
|
+
全离线:锁 stub 模型 + stub 嵌入 + 临时数据目录,不碰真实 API / 不加载真实向量模型。
|
|
4
|
+
与 test_multimodal.py(图片通道)互补:这里覆盖「文件」通道(图片走 images base64,不经过附件)。
|
|
5
|
+
"""
|
|
6
|
+
import os, sys, tempfile, shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
9
|
+
|
|
10
|
+
# ── 环境隔离:必须在 import foxclaw 之前(import 会读这些键初始化) ─────────────
|
|
11
|
+
for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
|
|
12
|
+
"GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
|
|
13
|
+
os.environ.pop(_k, None)
|
|
14
|
+
os.environ["HKC_EMBEDDING"] = "stub"
|
|
15
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
16
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_att_")
|
|
17
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
18
|
+
os.environ["FOXCLAW_PROVIDER"] = "" # 不指定 provider → 无 key 时降级 stub(确定性)
|
|
19
|
+
os.environ["FOXCLAW_MODEL"] = ""
|
|
20
|
+
os.environ["FOXCLAW_MCP_SERVER"] = "0"
|
|
21
|
+
os.environ["HF_HUB_OFFLINE"] = "1" # 离线:HMR embedding 读本地缓存,不联网
|
|
22
|
+
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
from fastapi.testclient import TestClient
|
|
26
|
+
|
|
27
|
+
from foxclaw.api import app, _sanitize_attachments
|
|
28
|
+
from foxclaw.api.routes.task import _MAX_ATTACHMENTS, _ATTACH_TEXT_MAX, _ATTACH_TEXT_TOTAL
|
|
29
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
30
|
+
from foxclaw.core.sessions import SessionStore
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture()
|
|
34
|
+
def store():
|
|
35
|
+
d = tempfile.mkdtemp(prefix="att_sess_")
|
|
36
|
+
try:
|
|
37
|
+
yield SessionStore(Path(d) / "sessions")
|
|
38
|
+
finally:
|
|
39
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_sanitize_attachments():
|
|
43
|
+
# 数量上限
|
|
44
|
+
many = [{"name": f"f{i}.txt", "text": "t"} for i in range(_MAX_ATTACHMENTS + 4)]
|
|
45
|
+
assert len(_sanitize_attachments(many)) == _MAX_ATTACHMENTS
|
|
46
|
+
|
|
47
|
+
# 单文件正文截断到上限
|
|
48
|
+
one = _sanitize_attachments([{"name": "a.txt", "text": "x" * (_ATTACH_TEXT_MAX + 100)}])
|
|
49
|
+
assert len(one[0]["text"]) == _ATTACH_TEXT_MAX
|
|
50
|
+
|
|
51
|
+
# name 截断到 200
|
|
52
|
+
assert _sanitize_attachments([{"name": "a" * 300, "text": "x"}])[0]["name"] == "a" * 200
|
|
53
|
+
|
|
54
|
+
# 累计正文封顶:单文件先截到 20000,4×20000 累计 60000 封顶 → 第 4 个正文被清空(名字仍在)
|
|
55
|
+
big = "x" * (_ATTACH_TEXT_MAX + 1000)
|
|
56
|
+
four = _sanitize_attachments([{"name": f"f{i}.txt", "text": big} for i in range(4)])
|
|
57
|
+
assert len(four) == 4
|
|
58
|
+
assert sum(len(a["text"]) for a in four) == _ATTACH_TEXT_TOTAL
|
|
59
|
+
assert four[3]["text"] == ""
|
|
60
|
+
|
|
61
|
+
# 非列表 / 非 dict / 空 name+空 text → 全部剔除
|
|
62
|
+
assert _sanitize_attachments("notalist") == []
|
|
63
|
+
assert _sanitize_attachments([1, "x"]) == []
|
|
64
|
+
assert _sanitize_attachments([{"name": "", "text": ""}]) == []
|
|
65
|
+
assert _sanitize_attachments(None) == []
|
|
66
|
+
|
|
67
|
+
# 字段类型清洗:size 非数字 → 0;mime/url 转 str
|
|
68
|
+
out = _sanitize_attachments([{"name": "a", "size": "big", "mime": None, "url": None, "text": "t"}])
|
|
69
|
+
assert out[0]["size"] == 0 and out[0]["mime"] == "" and out[0]["url"] == ""
|
|
70
|
+
print("护栏(数量/单文件/累计/类型清洗/边界)OK")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_attachments_context():
|
|
74
|
+
atts = [
|
|
75
|
+
{"name": "a.txt", "size": 11, "mime": "text/plain", "url": "", "text": "hello world"},
|
|
76
|
+
{"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "", "text": ""},
|
|
77
|
+
]
|
|
78
|
+
ctx = CognitiveAgent._attachments_context(atts)
|
|
79
|
+
assert "【附件「a.txt」内容如下】" in ctx and "hello world" in ctx
|
|
80
|
+
assert "【附件「b.txt」" not in ctx
|
|
81
|
+
assert "b.pdf" in ctx and "无法提取" in ctx # 非文本降级为「文件名 + 元数据」提示
|
|
82
|
+
assert CognitiveAgent._attachments_context([]) == ""
|
|
83
|
+
assert CognitiveAgent._attachments_context(None) == ""
|
|
84
|
+
assert CognitiveAgent._attachments_context([1, "x"]) == "" # 非 dict 全跳过
|
|
85
|
+
print("LLM 上下文注入(正文/降级/空)OK")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_sessions_append_attachments(store):
|
|
89
|
+
s = store.create()
|
|
90
|
+
atts = [
|
|
91
|
+
{"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc", "text": "正文不落盘"},
|
|
92
|
+
{"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "/api/attachments/def", "text": ""},
|
|
93
|
+
]
|
|
94
|
+
assert store.append(s["id"], "user", "读一下这两个文件", attachments=atts)
|
|
95
|
+
d = store.get(s["id"])
|
|
96
|
+
m = d["messages"][0]
|
|
97
|
+
assert m["role"] == "user" and m["content"] == "读一下这两个文件"
|
|
98
|
+
assert len(m["attachments"]) == 2
|
|
99
|
+
assert m["attachments"][0] == {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc"}
|
|
100
|
+
assert "text" not in m["attachments"][0], "text 不得落盘(会话文件不膨胀)"
|
|
101
|
+
# 不带附件 → 无 attachments 字段(向后兼容)
|
|
102
|
+
assert store.append(s["id"], "assistant", "好")
|
|
103
|
+
assert "attachments" not in store.get(s["id"])["messages"][1]
|
|
104
|
+
print("会话落盘(轻字段/不存 text/向后兼容)OK")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_upload_and_download():
|
|
108
|
+
with TestClient(app) as c:
|
|
109
|
+
# 上传文本文件 → 提取正文
|
|
110
|
+
r = c.post("/api/upload", files={"file": ("笔记.txt", "今天学了三件事".encode(), "text/plain")})
|
|
111
|
+
assert r.status_code == 200, r.text
|
|
112
|
+
a = r.json()
|
|
113
|
+
assert a["name"] == "笔记.txt" and a["text"] == "今天学了三件事"
|
|
114
|
+
assert a["url"].startswith("/api/attachments/") and a["id"] and a["size"] > 0
|
|
115
|
+
|
|
116
|
+
# 下载 → 同一内容
|
|
117
|
+
r2 = c.get(a["url"])
|
|
118
|
+
assert r2.status_code == 200
|
|
119
|
+
assert r2.content.decode("utf-8") == "今天学了三件事"
|
|
120
|
+
|
|
121
|
+
# 二进制(不可提取)→ text 为空,降级
|
|
122
|
+
r3 = c.post("/api/upload", files={"file": ("img.bin", b"\x00\x01\x02", "application/octet-stream")})
|
|
123
|
+
assert r3.status_code == 200 and r3.json()["text"] == ""
|
|
124
|
+
|
|
125
|
+
# 空文件 → 400;超大 → 413
|
|
126
|
+
assert c.post("/api/upload", files={"file": ("e.txt", b"", "text/plain")}).status_code == 400
|
|
127
|
+
big = b"x" * (21 * 1024 * 1024)
|
|
128
|
+
assert c.post("/api/upload", files={"file": ("big.txt", big, "text/plain")}).status_code == 413
|
|
129
|
+
|
|
130
|
+
# 路径穿越:HTTP 层规范化后到不了附件路由(404);非法 id 字符 → 400;合法但不存在 → 404
|
|
131
|
+
assert c.get("/api/attachments/../../etc/passwd").status_code == 404 # .. 被 URL 规范化,不匹配路由
|
|
132
|
+
assert c.get("/api/attachments/a.b").status_code == 400 # . 不在 id 白名单 [_A-Za-z0-9_-]
|
|
133
|
+
assert c.get("/api/attachments/deadbeef0000").status_code == 404 # 合法 id 但文件不存在
|
|
134
|
+
print("上传/下载(正文提取/二进制降级/空/超大/穿越/404)OK")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_chat_stream_with_attachments():
|
|
138
|
+
import foxclaw.api as _api
|
|
139
|
+
with TestClient(app) as c:
|
|
140
|
+
# 先上传一个带正文的附件,拿到引用
|
|
141
|
+
up = c.post("/api/upload", files={"file": ("要点.txt", "预算 100 万".encode(), "text/plain")})
|
|
142
|
+
att = up.json()
|
|
143
|
+
# 会话须先存在,append 才会落盘(前端 ensureSession 同款:POST /api/sessions → 拿 id)
|
|
144
|
+
sid = _api._os_instance.sessions.create()["id"]
|
|
145
|
+
payload = {
|
|
146
|
+
"task": "帮我总结要点",
|
|
147
|
+
"agent_id": "supervisor",
|
|
148
|
+
"session_id": sid,
|
|
149
|
+
"attachments": [{"name": att["name"], "size": att["size"], "mime": att["mime"],
|
|
150
|
+
"url": att["url"], "text": att["text"]}],
|
|
151
|
+
}
|
|
152
|
+
r = c.post("/api/chat/stream", json=payload)
|
|
153
|
+
assert r.status_code == 200, r.text[:200]
|
|
154
|
+
assert "data:" in r.text and '"error"' not in r.text, r.text[:300]
|
|
155
|
+
|
|
156
|
+
# 会话已落盘用户消息 + 附件元数据(不含 text)
|
|
157
|
+
d = _api._os_instance.sessions.get(sid)
|
|
158
|
+
assert d is not None, "会话未落盘"
|
|
159
|
+
um = d["messages"][0]
|
|
160
|
+
assert um["role"] == "user" and um["content"] == "帮我总结要点"
|
|
161
|
+
assert um["attachments"][0]["name"] == "要点.txt"
|
|
162
|
+
assert "text" not in um["attachments"][0]
|
|
163
|
+
print("/chat/stream 带附件端到端(SSE + 会话落盘)OK")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""审计落盘的哈希链 + recovery 日志脱敏的回归。
|
|
2
|
+
|
|
3
|
+
审计文件是纯 append 明文,谁能写谁就能改历史;哈希链不改可见性,只让「改一笔 / 删中间
|
|
4
|
+
一笔」在 verify 里暴露。recovery 日志则是被回放读回、长期留在磁盘上的工具调用记录,
|
|
5
|
+
工具参数里带 token 的情况很常见。
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _write_chain(path: Path, records) -> None:
|
|
15
|
+
from foxclaw.core import audit_chain
|
|
16
|
+
|
|
17
|
+
prev = audit_chain.GENESIS
|
|
18
|
+
lines = []
|
|
19
|
+
for r in records:
|
|
20
|
+
rec = audit_chain.link(r, prev)
|
|
21
|
+
prev = rec["hash"]
|
|
22
|
+
lines.append(json.dumps(rec, ensure_ascii=False))
|
|
23
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── 链 ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
def test_chain_passes(tmp_path):
|
|
30
|
+
from foxclaw.core import audit_chain
|
|
31
|
+
|
|
32
|
+
p = tmp_path / "a.jsonl"
|
|
33
|
+
_write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}, {"verdict": "success"}])
|
|
34
|
+
ok, n, msg = audit_chain.verify(p)
|
|
35
|
+
assert ok and n == 3, msg
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_tampered_line_detected(tmp_path):
|
|
39
|
+
from foxclaw.core import audit_chain
|
|
40
|
+
|
|
41
|
+
p = tmp_path / "a.jsonl"
|
|
42
|
+
_write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}])
|
|
43
|
+
lines = p.read_text(encoding="utf-8").splitlines()
|
|
44
|
+
rec = json.loads(lines[0])
|
|
45
|
+
rec["verdict"] = "allow" # 把一次拒绝抹成放行
|
|
46
|
+
lines[0] = json.dumps(rec, ensure_ascii=False)
|
|
47
|
+
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
ok, _, msg = audit_chain.verify(p)
|
|
50
|
+
assert not ok and "被改" in msg, msg
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_deleted_middle_line_detected(tmp_path):
|
|
54
|
+
from foxclaw.core import audit_chain
|
|
55
|
+
|
|
56
|
+
p = tmp_path / "a.jsonl"
|
|
57
|
+
_write_chain(p, [{"i": 0}, {"i": 1}, {"i": 2}])
|
|
58
|
+
lines = p.read_text(encoding="utf-8").splitlines()
|
|
59
|
+
p.write_text("\n".join([lines[0], lines[2]]) + "\n", encoding="utf-8")
|
|
60
|
+
|
|
61
|
+
ok, _, msg = audit_chain.verify(p)
|
|
62
|
+
assert not ok and "链断" in msg, msg
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_resume_after_restart(tmp_path):
|
|
66
|
+
"""进程重启后内存链尾丢失,新 auditor 应从文件尾部续链,而不是重开 GENESIS。"""
|
|
67
|
+
from foxclaw.core.governance_audit import GovernanceAuditor
|
|
68
|
+
|
|
69
|
+
p = tmp_path / "governance.jsonl"
|
|
70
|
+
GovernanceAuditor(sink_path=p).record(
|
|
71
|
+
layer="skill", actor="office", action="write_note", verdict="success")
|
|
72
|
+
aud = GovernanceAuditor(sink_path=p) # 模拟重启
|
|
73
|
+
aud.record(layer="skill", actor="office", action="write_note", verdict="success")
|
|
74
|
+
|
|
75
|
+
ok, n, msg = aud.verify()
|
|
76
|
+
assert ok and n == 2, msg
|
|
77
|
+
recs = aud.read()
|
|
78
|
+
assert recs[1]["hash"] != recs[0]["hash"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_legacy_lines_skipped(tmp_path):
|
|
82
|
+
"""老文件(无 hash)不能因为「没链」就报错;新记录从 GENESIS 起头。"""
|
|
83
|
+
from foxclaw.core import audit_chain
|
|
84
|
+
|
|
85
|
+
p = tmp_path / "a.jsonl"
|
|
86
|
+
p.write_text('{"legacy": true}\n', encoding="utf-8")
|
|
87
|
+
assert audit_chain.verify(p)[0] is True
|
|
88
|
+
assert audit_chain.tail_hash(p) == audit_chain.GENESIS
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_tail_hash_ignores_torn_line(tmp_path):
|
|
92
|
+
from foxclaw.core import audit_chain
|
|
93
|
+
|
|
94
|
+
p = tmp_path / "a.jsonl"
|
|
95
|
+
p.write_text('{"a": 1, "hash": "h1"}\n{"half', encoding="utf-8")
|
|
96
|
+
assert audit_chain.tail_hash(p) == "h1"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── recovery 脱敏 ──────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def test_recovery_step_args_and_result_redacted():
|
|
102
|
+
from types import SimpleNamespace
|
|
103
|
+
|
|
104
|
+
from foxclaw.agents.mixins.tool_exec import _record_step, _step_key
|
|
105
|
+
|
|
106
|
+
key = "sk-" + "a" * 24
|
|
107
|
+
got = {}
|
|
108
|
+
|
|
109
|
+
class _Rec:
|
|
110
|
+
@staticmethod
|
|
111
|
+
def record_step(tid, rec):
|
|
112
|
+
got.update(rec)
|
|
113
|
+
|
|
114
|
+
agent = SimpleNamespace(_recovery=_Rec(), _session_id="s1")
|
|
115
|
+
_record_step(agent, {"name": "http", "args": {"api_key": key}},
|
|
116
|
+
f"ok token=sk-{'b' * 24}")
|
|
117
|
+
|
|
118
|
+
assert "sk-" not in json.dumps(got["args"]), got["args"]
|
|
119
|
+
assert "[已脱敏]" in got["result"], got["result"]
|
|
120
|
+
# key 必须用未脱敏的 args 算,否则同一步骤前后 key 不一致(缓存/回放对不上)
|
|
121
|
+
assert got["key"] == _step_key("http", {"api_key": key})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""审计落盘 sink(audit_sink)测试 —— P0-3 配套
|
|
2
|
+
|
|
3
|
+
覆盖:
|
|
4
|
+
1. 单元层:EventSpine + sink 直连 —— JSONL 格式、字段齐全、追加语义、开关、坏行容错
|
|
5
|
+
2. 端到端层:真实 Foxclaw 装配 → 共享写入 → 文件里出现完整记录(接线正确性)
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
_FAILS = []
|
|
16
|
+
_N = 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def check(cond, msg):
|
|
20
|
+
global _N
|
|
21
|
+
_N += 1
|
|
22
|
+
print(f" {'✓' if cond else '✗'} {msg}")
|
|
23
|
+
if not cond:
|
|
24
|
+
_FAILS.append(msg)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_unit():
|
|
28
|
+
print("\n【1】单元层:EventSpine + sink 直连")
|
|
29
|
+
from foxclaw.kernel.spine import EventSpine
|
|
30
|
+
from foxclaw.core.audit_sink import SharedWriteAuditSink, attach, ENV_SWITCH
|
|
31
|
+
|
|
32
|
+
tmp = tempfile.mkdtemp(prefix="foxclaw_asink_")
|
|
33
|
+
try:
|
|
34
|
+
path = Path(tmp) / "audit" / "shared_write.jsonl"
|
|
35
|
+
spine = EventSpine()
|
|
36
|
+
sink = SharedWriteAuditSink(path)
|
|
37
|
+
spine.subscribe("memory.shared.write", sink)
|
|
38
|
+
|
|
39
|
+
spine.publish("memory.shared.write", {
|
|
40
|
+
"agent_id": "office", "type": "decision",
|
|
41
|
+
"tags": ["finance.transfer"], "allowed": False,
|
|
42
|
+
"reason": "office 的职责域不含 ['type:decision','finance.transfer']",
|
|
43
|
+
"mode": "audit", "content_sha": "0123456789abcdef",
|
|
44
|
+
"content_preview": "财务应给办公转账1000元"}, source="office")
|
|
45
|
+
# 非本主题的事件不应落盘(subscribe 前缀精确命中)
|
|
46
|
+
spine.publish("memory.updated", {"x": 1})
|
|
47
|
+
|
|
48
|
+
check(path.exists(), "审计文件已创建(含 audit/ 目录)")
|
|
49
|
+
recs = sink.read()
|
|
50
|
+
check(len(recs) == 1, f"只落了本主题 1 条(非本主题被过滤):实际 {len(recs)}")
|
|
51
|
+
r = recs[0]
|
|
52
|
+
check(r["topic"] == "memory.shared.write", "topic 正确")
|
|
53
|
+
check(r["payload"]["allowed"] is False, "payload 原样保留(allowed=False 可判读)")
|
|
54
|
+
check(len(r["payload"]["content_sha"]) == 16, "content_sha 为 16 位哈希")
|
|
55
|
+
check("ts" in r and "T" in r["ts"], "ts 为 ISO 时间(人类可读)")
|
|
56
|
+
check(bool(r["correlation_id"]), "correlation_id 已记录(可与脊柱 history 对账)")
|
|
57
|
+
|
|
58
|
+
# 追加语义:第二条接在后面,顺序保持
|
|
59
|
+
spine.publish("memory.shared.write", {"agent_id": "system", "type": "execution",
|
|
60
|
+
"tags": ["truth_pipeline"], "allowed": True,
|
|
61
|
+
"reason": "命中全局允许 facet", "mode": "audit",
|
|
62
|
+
"content_sha": "ffffffffffffffff",
|
|
63
|
+
"content_preview": "真值"})
|
|
64
|
+
recs = sink.read()
|
|
65
|
+
check(len(recs) == 2 and recs[1]["payload"]["agent_id"] == "system",
|
|
66
|
+
"追加写不覆盖旧行,顺序保持")
|
|
67
|
+
|
|
68
|
+
# 坏行容错:夹一行垃圾,read() 跳过而不是崩
|
|
69
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
70
|
+
f.write("{这不是JSON}\n")
|
|
71
|
+
check(len(sink.read()) == 2, "坏行被跳过,读取不崩")
|
|
72
|
+
|
|
73
|
+
# 开关:环境变量置 0 时 attach 不挂
|
|
74
|
+
old = os.environ.get(ENV_SWITCH)
|
|
75
|
+
os.environ[ENV_SWITCH] = "0"
|
|
76
|
+
try:
|
|
77
|
+
check(attach(EventSpine(), tmp) is None, "FOXCLAW_SHARED_WRITE_AUDIT=0 → 不挂")
|
|
78
|
+
os.environ[ENV_SWITCH] = "1"
|
|
79
|
+
s2 = attach(EventSpine(), tmp)
|
|
80
|
+
check(s2 is not None and s2.path == Path(tmp) / "audit" / "shared_write.jsonl",
|
|
81
|
+
"开启时挂载且路径跟随 data_dir")
|
|
82
|
+
finally:
|
|
83
|
+
if old is None:
|
|
84
|
+
os.environ.pop(ENV_SWITCH, None)
|
|
85
|
+
else:
|
|
86
|
+
os.environ[ENV_SWITCH] = old
|
|
87
|
+
finally:
|
|
88
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_end_to_end():
|
|
92
|
+
print("\n【2】端到端:真实 Foxclaw 装配 → 共享写入 → 落盘")
|
|
93
|
+
tmp = tempfile.mkdtemp(prefix="foxclaw_asink_e2e_")
|
|
94
|
+
try:
|
|
95
|
+
from foxclaw import Foxclaw
|
|
96
|
+
osi = Foxclaw(data_dir=tmp, shadow_rounds=5)
|
|
97
|
+
|
|
98
|
+
check(osi.shared_write_audit is not None, "sink 已随装配挂上(默认开启)")
|
|
99
|
+
audit_path = Path(tmp) / "audit" / "shared_write.jsonl"
|
|
100
|
+
check(audit_path.parent.exists(), "audit/ 目录已建在 data_dir 下")
|
|
101
|
+
|
|
102
|
+
# 一条合法 + 一条越界(audit 模式不拦截,只记录)——真实闸门口径
|
|
103
|
+
osi.hmr.ingest("用户偏好:喜欢表格", type="concept", agent_id="finance",
|
|
104
|
+
tags=["preference"], shared=True)
|
|
105
|
+
osi.hmr.ingest("财务应给办公转账1000元", type="decision", agent_id="office",
|
|
106
|
+
tags=["finance.transfer"], shared=True)
|
|
107
|
+
|
|
108
|
+
recs = osi.shared_write_audit.read()
|
|
109
|
+
check(len(recs) == 2, f"两笔共享写入都落盘:实际 {len(recs)}")
|
|
110
|
+
by_allowed = {r["payload"]["allowed"] for r in recs}
|
|
111
|
+
check(by_allowed == {True, False}, "allowed 的 True/False 都被原样记录(判读依据完整)")
|
|
112
|
+
denied = [r for r in recs if not r["payload"]["allowed"]]
|
|
113
|
+
check(bool(denied) and "office" in denied[0]["payload"]["reason"] or bool(denied),
|
|
114
|
+
"越界那条可从文件直接判读")
|
|
115
|
+
# 与脊柱 history 口径一致:文件条数 == 脊柱里该主题条数
|
|
116
|
+
spine_n = len(osi.event_spine.history("memory.shared.write", 100))
|
|
117
|
+
check(spine_n == len(recs), f"文件条数与脊柱 history 一致({len(recs)}=={spine_n})")
|
|
118
|
+
finally:
|
|
119
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main():
|
|
123
|
+
print("=" * 60)
|
|
124
|
+
print("审计落盘 sink(P0-3 配套)")
|
|
125
|
+
print("=" * 60)
|
|
126
|
+
test_unit()
|
|
127
|
+
test_end_to_end()
|
|
128
|
+
print("\n" + "=" * 60)
|
|
129
|
+
if _FAILS:
|
|
130
|
+
print(f"✗ {len(_FAILS)}/{_N} 项失败"); return 1
|
|
131
|
+
print(f"✓ 全部通过({_N} 项)")
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
raise SystemExit(main())
|