foxclaw 0.1.0 → 0.1.3
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 +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -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 +115 -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 +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -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 +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -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 +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -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 +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -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 +144 -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 +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -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 +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -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 +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -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 +1436 -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-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -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 +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- 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 +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -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 +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -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 +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -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 +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -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,97 @@
|
|
|
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
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
25
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
|
|
26
|
+
from foxclaw.api import app
|
|
27
|
+
|
|
28
|
+
with TestClient(app) as c:
|
|
29
|
+
print("="*64)
|
|
30
|
+
print("FOXCLAW · API 端到端验证 (ASGI TestClient)")
|
|
31
|
+
print("="*64)
|
|
32
|
+
|
|
33
|
+
r = c.get("/api/status")
|
|
34
|
+
print(f"\n[GET /api/status] HTTP {r.status_code}")
|
|
35
|
+
data = r.json()
|
|
36
|
+
for aid, s in data.items():
|
|
37
|
+
tc = s["thought_chain"]
|
|
38
|
+
print(f" {aid:<12} 节点={tc['total_nodes']:<3} shadow={s['harness']['shadow_mode']}")
|
|
39
|
+
|
|
40
|
+
print("\n[POST /api/task] 连发 7 个财务任务触发校准")
|
|
41
|
+
for i, t in enumerate(["分析支出","制定预算","评估风险","季度报告","追踪消费","明年规划","同期对比"], 1):
|
|
42
|
+
r = c.post("/api/task", json={"task": t, "agent_id": "finance"})
|
|
43
|
+
d = r.json()
|
|
44
|
+
err = d.get("error", "ok")
|
|
45
|
+
print(f" [{i}] {t} → HTTP {r.status_code} · {err}")
|
|
46
|
+
|
|
47
|
+
r = c.get("/api/harness/finance/status")
|
|
48
|
+
show("GET /api/harness/finance/status", r.json())
|
|
49
|
+
|
|
50
|
+
r = c.get("/api/thoughtchain/finance/chain")
|
|
51
|
+
nodes = r.json()["nodes"]
|
|
52
|
+
print(f"\n[GET /api/thoughtchain/finance/chain] HTTP {r.status_code} · {len(nodes)} 节点")
|
|
53
|
+
for n in nodes[-5:]:
|
|
54
|
+
print(f" {n['type']:<16} {n['harness_status']:<9} "
|
|
55
|
+
f"conf={n['conf']:.2f} Δ={n.get('harness_delta') or 0:.3f}")
|
|
56
|
+
|
|
57
|
+
r = c.get("/api/thoughtchain/finance/trend?limit=10")
|
|
58
|
+
trend = r.json()["trend"]
|
|
59
|
+
print(f"\n[GET .../trend] {len(trend)} 数据点 · Δ 序列:")
|
|
60
|
+
print(" " + " ".join(f"{t['delta']:.2f}" for t in trend[:10]))
|
|
61
|
+
|
|
62
|
+
r = c.get("/api/harness/finance/policy")
|
|
63
|
+
pol = r.json()
|
|
64
|
+
print(f"\n[GET /api/harness/finance/policy] v{pol['version']} · "
|
|
65
|
+
f"{len(pol.get('rules',[]))} 规则")
|
|
66
|
+
|
|
67
|
+
r = c.get("/api/preferences")
|
|
68
|
+
assert r.status_code == 200 and "preferences" in r.json(), r.text
|
|
69
|
+
r = c.delete("/api/preferences/nonexistent_id")
|
|
70
|
+
assert r.status_code == 200 and r.json().get("ok") is False, r.text
|
|
71
|
+
print("\n[GET/DELETE /api/preferences] 列出+删除通路 ok")
|
|
72
|
+
|
|
73
|
+
r = c.put("/api/profile", json={"nickname": " snow "})
|
|
74
|
+
assert r.status_code == 200 and r.json().get("nickname") == "snow", r.text
|
|
75
|
+
r = c.get("/api/profile")
|
|
76
|
+
assert r.json().get("nickname") == "snow", r.text
|
|
77
|
+
c.put("/api/profile", json={"nickname": "snow2"})
|
|
78
|
+
r = c.get("/api/preferences")
|
|
79
|
+
nicks = [p["content"] for p in r.json()["preferences"] if "称呼我为" in p["content"]]
|
|
80
|
+
assert nicks == ["用户偏好:称呼我为 snow2"], nicks
|
|
81
|
+
print("\n[GET/PUT /api/profile] 称呼写入/读回/替换 ok")
|
|
82
|
+
|
|
83
|
+
r = c.get("/api/system/info")
|
|
84
|
+
info = r.json()
|
|
85
|
+
assert r.status_code == 200 and "update" in info, r.text
|
|
86
|
+
up = info["update"]
|
|
87
|
+
assert {"enabled", "current", "latest", "update_available"} <= set(up), up
|
|
88
|
+
assert up["enabled"] is False and up["update_available"] is False, up
|
|
89
|
+
print("\n[GET /api/system/info] update 字段存在,默认关闭")
|
|
90
|
+
|
|
91
|
+
print("\n" + "="*64)
|
|
92
|
+
print("✓ API 端到端验证完成 · 所有端点返回真实数据")
|
|
93
|
+
print("="*64)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
test_api()
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
14
|
+
|
|
15
|
+
import tempfile
|
|
16
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
|
|
17
|
+
from fastapi.testclient import TestClient
|
|
18
|
+
from foxclaw.api import app
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check(cond, msg):
|
|
22
|
+
if not cond:
|
|
23
|
+
raise AssertionError(f"✗ {msg}")
|
|
24
|
+
print(f" ✓ {msg}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
print("=" * 56)
|
|
29
|
+
print("API 可选鉴权回归")
|
|
30
|
+
print("=" * 56)
|
|
31
|
+
os.environ.pop("FOXCLAW_API_KEY", None)
|
|
32
|
+
with TestClient(app) as c:
|
|
33
|
+
print("\n【1】未设 FOXCLAW_API_KEY:开放(本地默认)")
|
|
34
|
+
check(c.get("/api/status").status_code == 200, "/api/status 开放(200)")
|
|
35
|
+
|
|
36
|
+
print("\n【2】设了 FOXCLAW_API_KEY:/api/* 受保护")
|
|
37
|
+
os.environ["FOXCLAW_API_KEY"] = "secret-123"
|
|
38
|
+
check(c.get("/api/status").status_code == 401, "无 X-API-Key → 401")
|
|
39
|
+
check(c.get("/api/status", headers={"X-API-Key": "wrong"}).status_code == 401,
|
|
40
|
+
"错误 key → 401")
|
|
41
|
+
check(c.get("/api/status", headers={"X-API-Key": "secret-123"}).status_code == 200,
|
|
42
|
+
"正确 key → 200")
|
|
43
|
+
|
|
44
|
+
print("\n【3】WebSocket 鉴权(端点内 ?key=,http 中间件管不到 ws)")
|
|
45
|
+
rejected = False
|
|
46
|
+
try:
|
|
47
|
+
with c.websocket_connect("/ws/harness"):
|
|
48
|
+
pass
|
|
49
|
+
except Exception:
|
|
50
|
+
rejected = True
|
|
51
|
+
check(rejected, "WS 无 key → 握手被拒")
|
|
52
|
+
connected = False
|
|
53
|
+
try:
|
|
54
|
+
with c.websocket_connect("/ws/harness?key=secret-123"):
|
|
55
|
+
connected = True
|
|
56
|
+
except Exception:
|
|
57
|
+
connected = False
|
|
58
|
+
check(connected, "WS 带正确 ?key= → 连上")
|
|
59
|
+
|
|
60
|
+
print("\n【4】开鉴权时前端 / 文档仍开放")
|
|
61
|
+
check(c.get("/docs").status_code == 200, "/docs 无需 key(200)")
|
|
62
|
+
r_root = c.get("/")
|
|
63
|
+
check(r_root.status_code in (200, 404), f"/ 非 401(前端不被 key 挡, {r_root.status_code})")
|
|
64
|
+
|
|
65
|
+
print("\n【5】撤销 key → 恢复开放")
|
|
66
|
+
os.environ.pop("FOXCLAW_API_KEY", None)
|
|
67
|
+
check(c.get("/api/status").status_code == 200, "撤 key 后 /api/status 恢复开放")
|
|
68
|
+
|
|
69
|
+
print("\n" + "=" * 56)
|
|
70
|
+
print("API 鉴权回归通过")
|
|
71
|
+
print("=" * 56)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_api_auth():
|
|
75
|
+
main()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
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,151 @@
|
|
|
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
|
+
for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
|
|
11
|
+
"GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
|
|
12
|
+
os.environ.pop(_k, None)
|
|
13
|
+
os.environ["HKC_EMBEDDING"] = "stub"
|
|
14
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
15
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_att_")
|
|
16
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
17
|
+
os.environ["FOXCLAW_PROVIDER"] = ""
|
|
18
|
+
os.environ["FOXCLAW_MODEL"] = ""
|
|
19
|
+
os.environ["FOXCLAW_MCP_SERVER"] = "0"
|
|
20
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
21
|
+
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
from fastapi.testclient import TestClient
|
|
25
|
+
|
|
26
|
+
from foxclaw.api import app, _sanitize_attachments
|
|
27
|
+
from foxclaw.api.routes.task import _MAX_ATTACHMENTS, _ATTACH_TEXT_MAX, _ATTACH_TEXT_TOTAL
|
|
28
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
29
|
+
from foxclaw.core.sessions import SessionStore
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture()
|
|
33
|
+
def store():
|
|
34
|
+
d = tempfile.mkdtemp(prefix="att_sess_")
|
|
35
|
+
try:
|
|
36
|
+
yield SessionStore(Path(d) / "sessions")
|
|
37
|
+
finally:
|
|
38
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_sanitize_attachments():
|
|
42
|
+
many = [{"name": f"f{i}.txt", "text": "t"} for i in range(_MAX_ATTACHMENTS + 4)]
|
|
43
|
+
assert len(_sanitize_attachments(many)) == _MAX_ATTACHMENTS
|
|
44
|
+
|
|
45
|
+
one = _sanitize_attachments([{"name": "a.txt", "text": "x" * (_ATTACH_TEXT_MAX + 100)}])
|
|
46
|
+
assert len(one[0]["text"]) == _ATTACH_TEXT_MAX
|
|
47
|
+
|
|
48
|
+
assert _sanitize_attachments([{"name": "a" * 300, "text": "x"}])[0]["name"] == "a" * 200
|
|
49
|
+
|
|
50
|
+
big = "x" * (_ATTACH_TEXT_MAX + 1000)
|
|
51
|
+
four = _sanitize_attachments([{"name": f"f{i}.txt", "text": big} for i in range(4)])
|
|
52
|
+
assert len(four) == 4
|
|
53
|
+
assert sum(len(a["text"]) for a in four) == _ATTACH_TEXT_TOTAL
|
|
54
|
+
assert four[3]["text"] == ""
|
|
55
|
+
|
|
56
|
+
assert _sanitize_attachments("notalist") == []
|
|
57
|
+
assert _sanitize_attachments([1, "x"]) == []
|
|
58
|
+
assert _sanitize_attachments([{"name": "", "text": ""}]) == []
|
|
59
|
+
assert _sanitize_attachments(None) == []
|
|
60
|
+
|
|
61
|
+
out = _sanitize_attachments([{"name": "a", "size": "big", "mime": None, "url": None, "text": "t"}])
|
|
62
|
+
assert out[0]["size"] == 0 and out[0]["mime"] == "" and out[0]["url"] == ""
|
|
63
|
+
print("护栏(数量/单文件/累计/类型清洗/边界)OK")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_attachments_context():
|
|
67
|
+
atts = [
|
|
68
|
+
{"name": "a.txt", "size": 11, "mime": "text/plain", "url": "", "text": "hello world"},
|
|
69
|
+
{"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "", "text": ""},
|
|
70
|
+
]
|
|
71
|
+
ctx = CognitiveAgent._attachments_context(atts)
|
|
72
|
+
assert "【附件「a.txt」内容如下】" in ctx and "hello world" in ctx
|
|
73
|
+
assert "【附件「b.txt」" not in ctx
|
|
74
|
+
assert "b.pdf" in ctx and "无法提取" in ctx
|
|
75
|
+
assert CognitiveAgent._attachments_context([]) == ""
|
|
76
|
+
assert CognitiveAgent._attachments_context(None) == ""
|
|
77
|
+
assert CognitiveAgent._attachments_context([1, "x"]) == ""
|
|
78
|
+
print("LLM 上下文注入(正文/降级/空)OK")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_sessions_append_attachments(store):
|
|
82
|
+
s = store.create()
|
|
83
|
+
atts = [
|
|
84
|
+
{"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc", "text": "正文不落盘"},
|
|
85
|
+
{"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "/api/attachments/def", "text": ""},
|
|
86
|
+
]
|
|
87
|
+
assert store.append(s["id"], "user", "读一下这两个文件", attachments=atts)
|
|
88
|
+
d = store.get(s["id"])
|
|
89
|
+
m = d["messages"][0]
|
|
90
|
+
assert m["role"] == "user" and m["content"] == "读一下这两个文件"
|
|
91
|
+
assert len(m["attachments"]) == 2
|
|
92
|
+
assert m["attachments"][0] == {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc"}
|
|
93
|
+
assert "text" not in m["attachments"][0], "text 不得落盘(会话文件不膨胀)"
|
|
94
|
+
assert store.append(s["id"], "assistant", "好")
|
|
95
|
+
assert "attachments" not in store.get(s["id"])["messages"][1]
|
|
96
|
+
print("会话落盘(轻字段/不存 text/向后兼容)OK")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_upload_and_download():
|
|
100
|
+
with TestClient(app) as c:
|
|
101
|
+
r = c.post("/api/upload", files={"file": ("笔记.txt", "今天学了三件事".encode(), "text/plain")})
|
|
102
|
+
assert r.status_code == 200, r.text
|
|
103
|
+
a = r.json()
|
|
104
|
+
assert a["name"] == "笔记.txt" and a["text"] == "今天学了三件事"
|
|
105
|
+
assert a["url"].startswith("/api/attachments/") and a["id"] and a["size"] > 0
|
|
106
|
+
|
|
107
|
+
r2 = c.get(a["url"])
|
|
108
|
+
assert r2.status_code == 200
|
|
109
|
+
assert r2.content.decode("utf-8") == "今天学了三件事"
|
|
110
|
+
|
|
111
|
+
r3 = c.post("/api/upload", files={"file": ("img.bin", b"\x00\x01\x02", "application/octet-stream")})
|
|
112
|
+
assert r3.status_code == 200 and r3.json()["text"] == ""
|
|
113
|
+
|
|
114
|
+
assert c.post("/api/upload", files={"file": ("e.txt", b"", "text/plain")}).status_code == 400
|
|
115
|
+
big = b"x" * (21 * 1024 * 1024)
|
|
116
|
+
assert c.post("/api/upload", files={"file": ("big.txt", big, "text/plain")}).status_code == 413
|
|
117
|
+
|
|
118
|
+
assert c.get("/api/attachments/../../etc/passwd").status_code == 404
|
|
119
|
+
assert c.get("/api/attachments/a.b").status_code == 400
|
|
120
|
+
assert c.get("/api/attachments/deadbeef0000").status_code == 404
|
|
121
|
+
print("上传/下载(正文提取/二进制降级/空/超大/穿越/404)OK")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_chat_stream_with_attachments():
|
|
125
|
+
import foxclaw.api as _api
|
|
126
|
+
with TestClient(app) as c:
|
|
127
|
+
up = c.post("/api/upload", files={"file": ("要点.txt", "预算 100 万".encode(), "text/plain")})
|
|
128
|
+
att = up.json()
|
|
129
|
+
sid = _api.os_instance().sessions.create()["id"]
|
|
130
|
+
payload = {
|
|
131
|
+
"task": "帮我总结要点",
|
|
132
|
+
"agent_id": "supervisor",
|
|
133
|
+
"session_id": sid,
|
|
134
|
+
"attachments": [{"name": att["name"], "size": att["size"], "mime": att["mime"],
|
|
135
|
+
"url": att["url"], "text": att["text"]}],
|
|
136
|
+
}
|
|
137
|
+
r = c.post("/api/chat/stream", json=payload)
|
|
138
|
+
assert r.status_code == 200, r.text[:200]
|
|
139
|
+
assert "data:" in r.text and '"error"' not in r.text, r.text[:300]
|
|
140
|
+
|
|
141
|
+
d = _api.os_instance().sessions.get(sid)
|
|
142
|
+
assert d is not None, "会话未落盘"
|
|
143
|
+
um = d["messages"][0]
|
|
144
|
+
assert um["role"] == "user" and um["content"] == "帮我总结要点"
|
|
145
|
+
assert um["attachments"][0]["name"] == "要点.txt"
|
|
146
|
+
assert "text" not in um["attachments"][0]
|
|
147
|
+
print("/chat/stream 带附件端到端(SSE + 会话落盘)OK")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
def test_chain_passes(tmp_path):
|
|
28
|
+
from foxclaw.core import audit_chain
|
|
29
|
+
|
|
30
|
+
p = tmp_path / "a.jsonl"
|
|
31
|
+
_write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}, {"verdict": "success"}])
|
|
32
|
+
ok, n, msg = audit_chain.verify(p)
|
|
33
|
+
assert ok and n == 3, msg
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_tampered_line_detected(tmp_path):
|
|
37
|
+
from foxclaw.core import audit_chain
|
|
38
|
+
|
|
39
|
+
p = tmp_path / "a.jsonl"
|
|
40
|
+
_write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}])
|
|
41
|
+
lines = p.read_text(encoding="utf-8").splitlines()
|
|
42
|
+
rec = json.loads(lines[0])
|
|
43
|
+
rec["verdict"] = "allow"
|
|
44
|
+
lines[0] = json.dumps(rec, ensure_ascii=False)
|
|
45
|
+
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
46
|
+
|
|
47
|
+
ok, _, msg = audit_chain.verify(p)
|
|
48
|
+
assert not ok and "被改" in msg, msg
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_deleted_middle_line_detected(tmp_path):
|
|
52
|
+
from foxclaw.core import audit_chain
|
|
53
|
+
|
|
54
|
+
p = tmp_path / "a.jsonl"
|
|
55
|
+
_write_chain(p, [{"i": 0}, {"i": 1}, {"i": 2}])
|
|
56
|
+
lines = p.read_text(encoding="utf-8").splitlines()
|
|
57
|
+
p.write_text("\n".join([lines[0], lines[2]]) + "\n", encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
ok, _, msg = audit_chain.verify(p)
|
|
60
|
+
assert not ok and "链断" in msg, msg
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_resume_after_restart(tmp_path):
|
|
64
|
+
"""进程重启后内存链尾丢失,新 auditor 应从文件尾部续链,而不是重开 GENESIS。"""
|
|
65
|
+
from foxclaw.core.governance_audit import GovernanceAuditor
|
|
66
|
+
|
|
67
|
+
p = tmp_path / "governance.jsonl"
|
|
68
|
+
GovernanceAuditor(sink_path=p).record(
|
|
69
|
+
layer="skill", actor="office", action="write_note", verdict="success")
|
|
70
|
+
aud = GovernanceAuditor(sink_path=p)
|
|
71
|
+
aud.record(layer="skill", actor="office", action="write_note", verdict="success")
|
|
72
|
+
|
|
73
|
+
ok, n, msg = aud.verify()
|
|
74
|
+
assert ok and n == 2, msg
|
|
75
|
+
recs = aud.read()
|
|
76
|
+
assert recs[1]["hash"] != recs[0]["hash"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_legacy_lines_skipped(tmp_path):
|
|
80
|
+
"""老文件(无 hash)不能因为「没链」就报错;新记录从 GENESIS 起头。"""
|
|
81
|
+
from foxclaw.core import audit_chain
|
|
82
|
+
|
|
83
|
+
p = tmp_path / "a.jsonl"
|
|
84
|
+
p.write_text('{"legacy": true}\n', encoding="utf-8")
|
|
85
|
+
assert audit_chain.verify(p)[0] is True
|
|
86
|
+
assert audit_chain.tail_hash(p) == audit_chain.GENESIS
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_tail_hash_ignores_torn_line(tmp_path):
|
|
90
|
+
from foxclaw.core import audit_chain
|
|
91
|
+
|
|
92
|
+
p = tmp_path / "a.jsonl"
|
|
93
|
+
p.write_text('{"a": 1, "hash": "h1"}\n{"half', encoding="utf-8")
|
|
94
|
+
assert audit_chain.tail_hash(p) == "h1"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_recovery_step_args_and_result_redacted():
|
|
98
|
+
from types import SimpleNamespace
|
|
99
|
+
|
|
100
|
+
from foxclaw.agents.mixins.tool_exec import _record_step, _step_key
|
|
101
|
+
|
|
102
|
+
key = "sk-" + "a" * 24
|
|
103
|
+
got = {}
|
|
104
|
+
|
|
105
|
+
class _Rec:
|
|
106
|
+
@staticmethod
|
|
107
|
+
def record_step(tid, rec):
|
|
108
|
+
got.update(rec)
|
|
109
|
+
|
|
110
|
+
agent = SimpleNamespace(_recovery=_Rec(), _session_id="s1")
|
|
111
|
+
_record_step(agent, {"name": "http", "args": {"api_key": key}},
|
|
112
|
+
f"ok token=sk-{'b' * 24}")
|
|
113
|
+
|
|
114
|
+
assert "sk-" not in json.dumps(got["args"]), got["args"]
|
|
115
|
+
assert "[已脱敏]" in got["result"], got["result"]
|
|
116
|
+
assert got["key"] == _step_key("http", {"api_key": key})
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
spine.publish("memory.updated", {"x": 1})
|
|
46
|
+
|
|
47
|
+
check(path.exists(), "审计文件已创建(含 audit/ 目录)")
|
|
48
|
+
recs = sink.read()
|
|
49
|
+
check(len(recs) == 1, f"只落了本主题 1 条(非本主题被过滤):实际 {len(recs)}")
|
|
50
|
+
r = recs[0]
|
|
51
|
+
check(r["topic"] == "memory.shared.write", "topic 正确")
|
|
52
|
+
check(r["payload"]["allowed"] is False, "payload 原样保留(allowed=False 可判读)")
|
|
53
|
+
check(len(r["payload"]["content_sha"]) == 16, "content_sha 为 16 位哈希")
|
|
54
|
+
check("ts" in r and "T" in r["ts"], "ts 为 ISO 时间(人类可读)")
|
|
55
|
+
check(bool(r["correlation_id"]), "correlation_id 已记录(可与脊柱 history 对账)")
|
|
56
|
+
|
|
57
|
+
spine.publish("memory.shared.write", {"agent_id": "system", "type": "execution",
|
|
58
|
+
"tags": ["truth_pipeline"], "allowed": True,
|
|
59
|
+
"reason": "命中全局允许 facet", "mode": "audit",
|
|
60
|
+
"content_sha": "ffffffffffffffff",
|
|
61
|
+
"content_preview": "真值"})
|
|
62
|
+
recs = sink.read()
|
|
63
|
+
check(len(recs) == 2 and recs[1]["payload"]["agent_id"] == "system",
|
|
64
|
+
"追加写不覆盖旧行,顺序保持")
|
|
65
|
+
|
|
66
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
67
|
+
f.write("{这不是JSON}\n")
|
|
68
|
+
check(len(sink.read()) == 2, "坏行被跳过,读取不崩")
|
|
69
|
+
|
|
70
|
+
old = os.environ.get(ENV_SWITCH)
|
|
71
|
+
os.environ[ENV_SWITCH] = "0"
|
|
72
|
+
try:
|
|
73
|
+
check(attach(EventSpine(), tmp) is None, "FOXCLAW_SHARED_WRITE_AUDIT=0 → 不挂")
|
|
74
|
+
os.environ[ENV_SWITCH] = "1"
|
|
75
|
+
s2 = attach(EventSpine(), tmp)
|
|
76
|
+
check(s2 is not None and s2.path == Path(tmp) / "audit" / "shared_write.jsonl",
|
|
77
|
+
"开启时挂载且路径跟随 data_dir")
|
|
78
|
+
finally:
|
|
79
|
+
if old is None:
|
|
80
|
+
os.environ.pop(ENV_SWITCH, None)
|
|
81
|
+
else:
|
|
82
|
+
os.environ[ENV_SWITCH] = old
|
|
83
|
+
finally:
|
|
84
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_end_to_end():
|
|
88
|
+
print("\n【2】端到端:真实 Foxclaw 装配 → 共享写入 → 落盘")
|
|
89
|
+
tmp = tempfile.mkdtemp(prefix="foxclaw_asink_e2e_")
|
|
90
|
+
try:
|
|
91
|
+
from foxclaw import Foxclaw
|
|
92
|
+
osi = Foxclaw(data_dir=tmp, shadow_rounds=5)
|
|
93
|
+
|
|
94
|
+
check(osi.shared_write_audit is not None, "sink 已随装配挂上(默认开启)")
|
|
95
|
+
audit_path = Path(tmp) / "audit" / "shared_write.jsonl"
|
|
96
|
+
check(audit_path.parent.exists(), "audit/ 目录已建在 data_dir 下")
|
|
97
|
+
|
|
98
|
+
osi.hmr.ingest("用户偏好:喜欢表格", type="concept", agent_id="finance",
|
|
99
|
+
tags=["preference"], shared=True)
|
|
100
|
+
osi.hmr.ingest("财务应给办公转账1000元", type="decision", agent_id="office",
|
|
101
|
+
tags=["finance.transfer"], shared=True)
|
|
102
|
+
|
|
103
|
+
recs = osi.shared_write_audit.read()
|
|
104
|
+
check(len(recs) == 2, f"两笔共享写入都落盘:实际 {len(recs)}")
|
|
105
|
+
by_allowed = {r["payload"]["allowed"] for r in recs}
|
|
106
|
+
check(by_allowed == {True, False}, "allowed 的 True/False 都被原样记录(判读依据完整)")
|
|
107
|
+
denied = [r for r in recs if not r["payload"]["allowed"]]
|
|
108
|
+
check(bool(denied) and "office" in denied[0]["payload"]["reason"] or bool(denied),
|
|
109
|
+
"越界那条可从文件直接判读")
|
|
110
|
+
spine_n = len(osi.event_spine.history("memory.shared.write", 100))
|
|
111
|
+
check(spine_n == len(recs), f"文件条数与脊柱 history 一致({len(recs)}=={spine_n})")
|
|
112
|
+
finally:
|
|
113
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main():
|
|
117
|
+
print("=" * 60)
|
|
118
|
+
print("审计落盘 sink(P0-3 配套)")
|
|
119
|
+
print("=" * 60)
|
|
120
|
+
test_unit()
|
|
121
|
+
test_end_to_end()
|
|
122
|
+
print("\n" + "=" * 60)
|
|
123
|
+
if _FAILS:
|
|
124
|
+
print(f"✗ {len(_FAILS)}/{_N} 项失败"); return 1
|
|
125
|
+
print(f"✓ 全部通过({_N} 项)")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
raise SystemExit(main())
|