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,143 @@
|
|
|
1
|
+
"""浏览器导航与请求的 URL 准入策略。
|
|
2
|
+
|
|
3
|
+
背景:ActionExecutor 原本「只做翻译不做判断」,`navigate` 直接把模型给的字符串
|
|
4
|
+
喂给 driver → 可被用来打 `file://`(读本机文件)、`javascript:`(在页面上下文执行)、
|
|
5
|
+
`http://169.254.169.254`(云元数据)或内网地址(SSRF 探测)。
|
|
6
|
+
|
|
7
|
+
两层,缺一不可:
|
|
8
|
+
- `validate_navigate_url` —— 模型显式要求的导航目标(动作层)。
|
|
9
|
+
- `validate_request_url` —— 页面上**任何**网络请求:3xx 跳转后的新地址、iframe、
|
|
10
|
+
子资源(img/script/fetch)。只校验导航目标时,一个公网页面跳一次转、或塞一个
|
|
11
|
+
`<img src="http://169.254.169.254/latest/meta-data/">` 就绕过去了。
|
|
12
|
+
|
|
13
|
+
默认 deny-by-default,允许内网时用 FOXCLAW_BROWSER_ALLOW_PRIVATE=1 显式放开。
|
|
14
|
+
|
|
15
|
+
【残余风险 · DNS TOCTOU】本层校验时解析一次域名,浏览器之后会**自己再解析一次**。
|
|
16
|
+
DNS rebinding 可以让第一次返回公网、第二次返回 127.0.0.1。彻底消除得在启动参数里
|
|
17
|
+
把域名钉成 IP(Chromium `--host-resolver-rules`)或下沉到 CDP 层 —— Playwright 的
|
|
18
|
+
route 回调拿不到「浏览器将要连接哪个 IP」。本层把风险从「整个会话不校验」收窄到
|
|
19
|
+
「每次请求校验一次」,不声称消除。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import ipaddress
|
|
25
|
+
import os
|
|
26
|
+
import socket
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
from urllib.parse import urlparse
|
|
30
|
+
|
|
31
|
+
ALLOWED_SCHEMES = ("http", "https")
|
|
32
|
+
|
|
33
|
+
INTERNAL_SCHEMES = ("data", "blob", "about")
|
|
34
|
+
|
|
35
|
+
_RESOLVE_TTL = 60.0
|
|
36
|
+
_MAX_CACHE = 512
|
|
37
|
+
_resolve_cache: dict[str, tuple[bool, float]] = {}
|
|
38
|
+
_resolve_lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _allow_private() -> bool:
|
|
42
|
+
return os.environ.get("FOXCLAW_BROWSER_ALLOW_PRIVATE", "").strip() in ("1", "true", "yes")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_internal_ip(ip) -> bool:
|
|
46
|
+
return (ip.is_loopback or ip.is_private or ip.is_link_local
|
|
47
|
+
or ip.is_reserved or ip.is_multicast or ip.is_unspecified)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_internal(host: str) -> bool:
|
|
51
|
+
"""域名是否解析到内网地址。解析失败按「未知」处理 → False(交给浏览器失败)。
|
|
52
|
+
|
|
53
|
+
不缓存失败结果:离线/瞬时 DNS 故障后恢复,没必要让页面一分钟内都起不来。
|
|
54
|
+
"""
|
|
55
|
+
now = time.monotonic()
|
|
56
|
+
with _resolve_lock:
|
|
57
|
+
hit = _resolve_cache.get(host)
|
|
58
|
+
if hit is not None and hit[1] > now:
|
|
59
|
+
return hit[0]
|
|
60
|
+
try:
|
|
61
|
+
infos = socket.getaddrinfo(host, None)
|
|
62
|
+
except OSError:
|
|
63
|
+
return False
|
|
64
|
+
internal = False
|
|
65
|
+
for info in infos:
|
|
66
|
+
addr = str(info[4][0]).split("%", 1)[0]
|
|
67
|
+
try:
|
|
68
|
+
parsed = ipaddress.ip_address(addr)
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
if _is_internal_ip(parsed):
|
|
72
|
+
internal = True
|
|
73
|
+
break
|
|
74
|
+
with _resolve_lock:
|
|
75
|
+
if len(_resolve_cache) >= _MAX_CACHE:
|
|
76
|
+
_resolve_cache.clear()
|
|
77
|
+
_resolve_cache[host] = (internal, now + _RESOLVE_TTL)
|
|
78
|
+
return internal
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _deny_reason(host: str) -> str | None:
|
|
82
|
+
"""host 命中本机/内网 → 返回拒绝原因;合法公网主机 → None。"""
|
|
83
|
+
try:
|
|
84
|
+
ip = ipaddress.ip_address(host)
|
|
85
|
+
except ValueError:
|
|
86
|
+
ip = None
|
|
87
|
+
if ip is not None:
|
|
88
|
+
if _is_internal_ip(ip):
|
|
89
|
+
return (f"已拒绝内网/本机地址 {host}(如确需访问请设 "
|
|
90
|
+
f"FOXCLAW_BROWSER_ALLOW_PRIVATE=1)")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
lowered = host.lower()
|
|
94
|
+
if lowered == "localhost" or lowered.endswith(".localhost"):
|
|
95
|
+
return "已拒绝本机域名 localhost(SSRF 防护)"
|
|
96
|
+
if _resolve_internal(host):
|
|
97
|
+
return f"已拒绝域名 {host}(解析到内网地址)"
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_navigate_url(url: str, allow_private: bool | None = None) -> str:
|
|
102
|
+
"""校验并规范化导航目标;不合法抛 ValueError(调用方转成用户可读错误)。
|
|
103
|
+
|
|
104
|
+
允许:http/https + 公网主机。
|
|
105
|
+
拒绝:其它协议(file/javascript/data/blob…)、无主机、以及(默认)回环/私网/
|
|
106
|
+
链路本地/保留地址。域名会尝试解析一次以拦「域名指向内网」;解析失败不拦
|
|
107
|
+
(离线场景下页面本来也打不开,不该因此拒掉合法 URL)。
|
|
108
|
+
"""
|
|
109
|
+
target = (url or "").strip()
|
|
110
|
+
if not target:
|
|
111
|
+
raise ValueError("导航目标为空")
|
|
112
|
+
parsed = urlparse(target)
|
|
113
|
+
scheme = (parsed.scheme or "").lower()
|
|
114
|
+
if scheme not in ALLOWED_SCHEMES:
|
|
115
|
+
raise ValueError(f"只允许 http/https 导航,已拒绝 {scheme or '(无协议)'}://")
|
|
116
|
+
host = (parsed.hostname or "").strip()
|
|
117
|
+
if not host:
|
|
118
|
+
raise ValueError("导航 URL 缺少主机名")
|
|
119
|
+
if allow_private is None:
|
|
120
|
+
allow_private = _allow_private()
|
|
121
|
+
if allow_private:
|
|
122
|
+
return target
|
|
123
|
+
|
|
124
|
+
reason = _deny_reason(host)
|
|
125
|
+
if reason:
|
|
126
|
+
raise ValueError(reason)
|
|
127
|
+
return target
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_request_url(url: str, allow_private: bool | None = None) -> str:
|
|
131
|
+
"""单个网络请求(子资源 / 跳转 / iframe)的准入。
|
|
132
|
+
|
|
133
|
+
与 `validate_navigate_url` 的唯一差别是豁免页面内部 scheme
|
|
134
|
+
(`data:` / `blob:` / `about:`):它们由页面自身产生、不经过网络栈。
|
|
135
|
+
其余规则完全一致 —— 跳转后的地址、iframe、img/script 都要过这一关。
|
|
136
|
+
"""
|
|
137
|
+
target = (url or "").strip()
|
|
138
|
+
if not target:
|
|
139
|
+
raise ValueError("请求 URL 为空")
|
|
140
|
+
scheme = (urlparse(target).scheme or "").lower()
|
|
141
|
+
if scheme in INTERNAL_SCHEMES:
|
|
142
|
+
return target
|
|
143
|
+
return validate_navigate_url(target, allow_private)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""适配器层:与 HIR-Kernel 约定的 WebAdapter 门面(Foxclaw 内改走 BrowserService)。"""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BaseAdapter —— 与 HIR v1.0 约定的 Adapter 接口保持一致,
|
|
3
|
+
方便直接 `vm.register_adapter(WebAdapter())` 接入 HIR Kernel。
|
|
4
|
+
|
|
5
|
+
如果你的 HIR Core 里已经有 hir.adapters.base.BaseAdapter,
|
|
6
|
+
直接 import 那一份替换这里的定义即可,这里只是保证
|
|
7
|
+
HIR-Browser 单独运行/测试时不依赖外部 HIR 包。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseAdapter(ABC):
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def match(self, action: dict) -> bool:
|
|
17
|
+
"""判断这个 Action Graph 节点是否应该由本 adapter 处理"""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def execute(self, action: dict) -> dict:
|
|
22
|
+
"""执行动作,返回归一化前的原始结果"""
|
|
23
|
+
...
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebAdapter —— HIR-Browser 对外的 HIR-Kernel 门面(match/execute 约定)。
|
|
3
|
+
|
|
4
|
+
HIR ExecutionVM 只认识这一个类。所有内部的
|
|
5
|
+
perception/action/driver/session 复杂度都被封装在这里,
|
|
6
|
+
不向 HIR Core 泄露任何实现细节。
|
|
7
|
+
|
|
8
|
+
注意:在 Foxclaw 内,async 路由请走 BrowserService(专用线程 + async 包装),
|
|
9
|
+
不要直接在事件循环线程里调用本类的 execute(Playwright sync API 会报错)。
|
|
10
|
+
本类保留是为了与独立 HIR-Kernel 场景兼容。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .base_adapter import BaseAdapter
|
|
14
|
+
from ..session.session_manager import SessionManager
|
|
15
|
+
from ..action.action_types import WebAction, SUPPORTED_ACTIONS
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebAdapter(BaseAdapter):
|
|
19
|
+
|
|
20
|
+
def __init__(self, headless: bool = True):
|
|
21
|
+
self._session: "SessionManager | None" = None
|
|
22
|
+
self._headless = headless
|
|
23
|
+
|
|
24
|
+
def _ensure_session(self):
|
|
25
|
+
if self._session is None:
|
|
26
|
+
self._session = SessionManager(headless=self._headless)
|
|
27
|
+
return self._session
|
|
28
|
+
|
|
29
|
+
def match(self, action: dict) -> bool:
|
|
30
|
+
return action.get("type") in SUPPORTED_ACTIONS
|
|
31
|
+
|
|
32
|
+
def execute(self, action: dict) -> dict:
|
|
33
|
+
session = self._ensure_session()
|
|
34
|
+
web_action = WebAction.from_dict(action)
|
|
35
|
+
result = session.execute(web_action)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"type": "web_adapter_result",
|
|
39
|
+
"action": action,
|
|
40
|
+
"data": result,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def close(self):
|
|
44
|
+
if self._session is not None:
|
|
45
|
+
self._session.close()
|
|
46
|
+
self._session = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""驱动层:抽象 BaseDriver + 默认 PlaywrightDriver。playwright 为可选依赖,惰性导入。"""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BaseDriver —— 底层浏览器驱动的抽象接口。
|
|
3
|
+
|
|
4
|
+
driver 层的职责边界:只负责"怎么操纵一个真实浏览器",
|
|
5
|
+
不关心语义压缩、不关心动作校验、不关心 Action Graph。
|
|
6
|
+
|
|
7
|
+
现在提供 PlaywrightDriver 作为默认实现。
|
|
8
|
+
以后如果要换成 CDP 直连或反检测方案,只需要新写一个
|
|
9
|
+
继承 BaseDriver 的类,上层(perception/action)完全不用改。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import List, Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseDriver(ABC):
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def navigate(self, url: str) -> None:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def query_interactive_elements(self, roles: tuple) -> List[Any]:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def describe_element(self, handle: Any) -> dict:
|
|
28
|
+
"""返回 {"role": str, "text": str, "attrs": dict}"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def click(self, handle: Any) -> None:
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def type_text(self, handle: Any, text: str) -> None:
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def scroll(self, direction: str) -> None:
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def wait(self, seconds: float) -> None:
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def get_visible_text(self) -> str:
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def current_url(self) -> str:
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def current_title(self) -> str:
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def close(self) -> None:
|
|
61
|
+
...
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlaywrightDriver —— 默认底层驱动实现。
|
|
3
|
+
|
|
4
|
+
用 Playwright 的 sync API,因为 HIR v1.0 Kernel 的执行流程本身是
|
|
5
|
+
顺序同步的(详见 HIR ExecutionVM 设计:v1 不做并发调度)。
|
|
6
|
+
如果以后要支持并发多会话,再切换到 async API 或换 CDP driver。
|
|
7
|
+
|
|
8
|
+
注意:这里刻意用 role 相关的 CSS/属性选择器组合来找"可交互元素",
|
|
9
|
+
没有依赖 accessibility tree API(虽然更准确,但部分 headless 场景下
|
|
10
|
+
获取成本更高),这是 v1 的工程妥协,标注在这里方便后续替换。
|
|
11
|
+
|
|
12
|
+
【融合适配 A1/A2】
|
|
13
|
+
- playwright 在 Foxclaw 里是**可选依赖**(extra `browser`),所以 import
|
|
14
|
+
刻意放进 __init__ 而非模块顶层:仅"真正构造 driver"时才需要 playwright,
|
|
15
|
+
`import foxclaw.browser` 本身不拖 playwright,缺失时子系统优雅降级而非崩溃。
|
|
16
|
+
- Playwright sync API **不能在 asyncio 事件循环线程里跑**,且其对象有线程亲和性;
|
|
17
|
+
故本 driver 必须由 BrowserService 的专用工作线程独占构造与调用(见 service.py)。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
from typing import List, Any
|
|
23
|
+
|
|
24
|
+
from .base_driver import BaseDriver
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_MAX_BLOCKED = 50
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
ROLE_SELECTOR_MAP = {
|
|
31
|
+
"button": "button, [role=button], input[type=button], input[type=submit]",
|
|
32
|
+
"link": "a[href]",
|
|
33
|
+
"textbox": "input[type=text], input[type=search], input[type=email], "
|
|
34
|
+
"input[type=password], input:not([type]), textarea",
|
|
35
|
+
"checkbox": "input[type=checkbox]",
|
|
36
|
+
"radio": "input[type=radio]",
|
|
37
|
+
"combobox": "select",
|
|
38
|
+
"menuitem": "[role=menuitem]",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PlaywrightDriver(BaseDriver):
|
|
43
|
+
|
|
44
|
+
def __init__(self, headless: bool = True, guard_requests: bool | None = None):
|
|
45
|
+
from playwright.sync_api import sync_playwright
|
|
46
|
+
|
|
47
|
+
self._pw = None
|
|
48
|
+
self._browser = None
|
|
49
|
+
self._page = None
|
|
50
|
+
self.blocked_requests: list[tuple[str, str]] = []
|
|
51
|
+
try:
|
|
52
|
+
self._pw = sync_playwright().start()
|
|
53
|
+
self._browser = self._pw.chromium.launch(headless=headless)
|
|
54
|
+
self._page = self._browser.new_page()
|
|
55
|
+
if guard_requests is None:
|
|
56
|
+
guard_requests = os.environ.get(
|
|
57
|
+
"FOXCLAW_BROWSER_GUARD_REQUESTS", "1").strip() not in ("0", "false", "no")
|
|
58
|
+
if guard_requests:
|
|
59
|
+
self._page.route("**/*", self._on_route)
|
|
60
|
+
except Exception:
|
|
61
|
+
self._safe_teardown()
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
def _on_route(self, route) -> None:
|
|
65
|
+
"""路由层准入:放行公网 http(s),拦下内网 / 非 http(s) 请求。
|
|
66
|
+
|
|
67
|
+
Playwright 的 sync 回调在浏览器真正发请求**之前**执行,所以 abort 能挡住
|
|
68
|
+
连接建立,而不是等响应回来才后悔。DNS 层面的 TOCTOU 见 url_policy 的说明。
|
|
69
|
+
"""
|
|
70
|
+
from ..action.url_policy import validate_request_url
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
url = route.request.url or ""
|
|
74
|
+
except Exception:
|
|
75
|
+
url = ""
|
|
76
|
+
try:
|
|
77
|
+
validate_request_url(url)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
self.blocked_requests.append((url[:512], str(e)))
|
|
80
|
+
if len(self.blocked_requests) > _MAX_BLOCKED:
|
|
81
|
+
del self.blocked_requests[:-_MAX_BLOCKED]
|
|
82
|
+
try:
|
|
83
|
+
route.abort("blockedbyclient")
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return
|
|
87
|
+
try:
|
|
88
|
+
route.continue_()
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
def _safe_teardown(self):
|
|
93
|
+
"""尽力释放已创建的资源,任一步失败都不影响其余步骤。"""
|
|
94
|
+
for closer in (
|
|
95
|
+
lambda: self._browser.close() if self._browser else None,
|
|
96
|
+
lambda: self._pw.stop() if self._pw else None,
|
|
97
|
+
):
|
|
98
|
+
try:
|
|
99
|
+
closer()
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
self._page = None
|
|
103
|
+
self._browser = None
|
|
104
|
+
self._pw = None
|
|
105
|
+
|
|
106
|
+
def _require_page(self):
|
|
107
|
+
"""取当前页面;未打开或已拆卸时立刻报错,而不是等 None 属性访问炸。
|
|
108
|
+
|
|
109
|
+
`_page` 的静态类型是 `Page | None`(初始化与拆卸都会置 None)。下面几个
|
|
110
|
+
方法原先直接 `self._page.xxx`:正常流程里 close() 之后没人再调,所以一直
|
|
111
|
+
没出事;一旦有人在拆卸后调用,得到的是 `'NoneType' object has no attribute
|
|
112
|
+
'goto'`,看不出「会话已关闭」这层意思。集中在这里判一次,报错即意图。
|
|
113
|
+
"""
|
|
114
|
+
if self._page is None:
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
"browser session is not open: driver was closed or failed to launch"
|
|
117
|
+
)
|
|
118
|
+
return self._page
|
|
119
|
+
|
|
120
|
+
def navigate(self, url: str) -> None:
|
|
121
|
+
self._require_page().goto(url, wait_until="domcontentloaded")
|
|
122
|
+
|
|
123
|
+
def query_interactive_elements(self, roles: tuple) -> List[Any]:
|
|
124
|
+
selectors = [ROLE_SELECTOR_MAP[r] for r in roles if r in ROLE_SELECTOR_MAP]
|
|
125
|
+
combined = ", ".join(selectors)
|
|
126
|
+
elements = self._require_page().query_selector_all(combined)
|
|
127
|
+
return [el for el in elements if el.is_visible()]
|
|
128
|
+
|
|
129
|
+
def describe_element(self, handle) -> dict:
|
|
130
|
+
role = handle.evaluate(
|
|
131
|
+
"el => el.getAttribute('role') || el.tagName.toLowerCase()"
|
|
132
|
+
)
|
|
133
|
+
text = handle.evaluate(
|
|
134
|
+
"el => (el.innerText || el.value || el.placeholder || '').trim()"
|
|
135
|
+
)
|
|
136
|
+
attrs = handle.evaluate(
|
|
137
|
+
"""el => {
|
|
138
|
+
const out = {};
|
|
139
|
+
for (const attr of ['href', 'name', 'type', 'aria-label']) {
|
|
140
|
+
if (el.getAttribute(attr)) out[attr] = el.getAttribute(attr);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}"""
|
|
144
|
+
)
|
|
145
|
+
return {"role": role, "text": text, "attrs": attrs}
|
|
146
|
+
|
|
147
|
+
def click(self, handle) -> None:
|
|
148
|
+
handle.click()
|
|
149
|
+
|
|
150
|
+
def type_text(self, handle, text: str) -> None:
|
|
151
|
+
handle.fill(text)
|
|
152
|
+
|
|
153
|
+
def scroll(self, direction: str) -> None:
|
|
154
|
+
delta = 600 if direction == "down" else -600
|
|
155
|
+
self._require_page().mouse.wheel(0, delta)
|
|
156
|
+
|
|
157
|
+
def wait(self, seconds: float) -> None:
|
|
158
|
+
time.sleep(seconds)
|
|
159
|
+
|
|
160
|
+
def get_visible_text(self) -> str:
|
|
161
|
+
return self._require_page().inner_text("body")
|
|
162
|
+
|
|
163
|
+
def current_url(self) -> str:
|
|
164
|
+
return self._require_page().url
|
|
165
|
+
|
|
166
|
+
def current_title(self) -> str:
|
|
167
|
+
return self._require_page().title()
|
|
168
|
+
|
|
169
|
+
def close(self) -> None:
|
|
170
|
+
self._safe_teardown()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""感知层:DOM 抽取 → 编号 → 语义压缩 → 快照。不依赖任何具体 driver。"""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DOM 提取器。
|
|
3
|
+
|
|
4
|
+
职责边界很窄:只负责"从底层 driver 拿原始数据",
|
|
5
|
+
不做任何压缩/裁剪——压缩是 SemanticReducer 的事。
|
|
6
|
+
这样分层是为了让"取数方式"和"压缩策略"可以独立演化
|
|
7
|
+
(比如以后换成 CDP driver,只需要改这一层)。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
INTERACTIVE_ROLES = ("button", "link", "textbox", "checkbox", "radio", "combobox", "menuitem")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DomExtractor:
|
|
14
|
+
|
|
15
|
+
def __init__(self, driver):
|
|
16
|
+
self.driver = driver
|
|
17
|
+
|
|
18
|
+
def extract_interactive_elements(self):
|
|
19
|
+
"""
|
|
20
|
+
返回底层元素句柄列表(具体类型由 driver 决定,Playwright 下是 ElementHandle)。
|
|
21
|
+
只取可见 + 可交互的元素,不可见的直接丢弃,避免 LLM 尝试点击看不见的东西。
|
|
22
|
+
"""
|
|
23
|
+
return self.driver.query_interactive_elements(INTERACTIVE_ROLES)
|
|
24
|
+
|
|
25
|
+
def describe_element(self, handle) -> dict:
|
|
26
|
+
"""把一个元素句柄转成 role/text/attrs 的原始描述,交给 indexer 二次加工"""
|
|
27
|
+
return self.driver.describe_element(handle)
|
|
28
|
+
|
|
29
|
+
def extract_visible_text(self, max_chars: int = 2000) -> str:
|
|
30
|
+
"""
|
|
31
|
+
提取页面主要可见文本,做长度上限控制。
|
|
32
|
+
这不是给 LLM 做阅读理解用的全文,只是辅助上下文,
|
|
33
|
+
真正结构化的信息应该来自 elements 列表。
|
|
34
|
+
"""
|
|
35
|
+
text = self.driver.get_visible_text()
|
|
36
|
+
return text[:max_chars]
|
|
37
|
+
|
|
38
|
+
def current_url(self) -> str:
|
|
39
|
+
return self.driver.current_url()
|
|
40
|
+
|
|
41
|
+
def current_title(self) -> str:
|
|
42
|
+
return self.driver.current_title()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
元素编号器。
|
|
3
|
+
|
|
4
|
+
核心思路:LLM 不应该猜 CSS selector 或坐标,只需要输出一个数字 ID。
|
|
5
|
+
这里负责:给页面里当前可交互的元素分配编号,并维护
|
|
6
|
+
"编号 -> 底层浏览器句柄"的映射,供 action_executor 使用。
|
|
7
|
+
|
|
8
|
+
编号在同一个 Snapshot 生命周期内稳定;页面一旦发生导航或大幅刷新,
|
|
9
|
+
编号需要重新分配(旧编号即失效,这是有意为之——避免"过期编号"
|
|
10
|
+
被误用去点击一个已经不存在的元素)。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import List, Dict, Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ElementIndexer:
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._registry: Dict[str, Any] = {}
|
|
20
|
+
self._counter = 0
|
|
21
|
+
|
|
22
|
+
def reset(self):
|
|
23
|
+
self._registry.clear()
|
|
24
|
+
self._counter = 0
|
|
25
|
+
|
|
26
|
+
def assign(self, handles: List[Any], describe_fn) -> List[Dict[str, Any]]:
|
|
27
|
+
"""
|
|
28
|
+
handles: 底层 driver 返回的可交互元素句柄列表
|
|
29
|
+
describe_fn: 把单个句柄转成 {"role":..., "text":..., "attrs":...} 的函数,
|
|
30
|
+
由具体 driver 实现(不同 driver 取属性的方式不同)
|
|
31
|
+
"""
|
|
32
|
+
self.reset()
|
|
33
|
+
indexed = []
|
|
34
|
+
|
|
35
|
+
for handle in handles:
|
|
36
|
+
self._counter += 1
|
|
37
|
+
eid = str(self._counter)
|
|
38
|
+
self._registry[eid] = handle
|
|
39
|
+
|
|
40
|
+
desc = describe_fn(handle)
|
|
41
|
+
indexed.append({
|
|
42
|
+
"id": eid,
|
|
43
|
+
"role": desc.get("role", "unknown"),
|
|
44
|
+
"text": desc.get("text", "")[:80],
|
|
45
|
+
"attrs": desc.get("attrs", {}),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return indexed
|
|
49
|
+
|
|
50
|
+
def resolve(self, element_id: str):
|
|
51
|
+
"""把编号还原为底层句柄,供 action_executor 执行真实操作"""
|
|
52
|
+
if element_id not in self._registry:
|
|
53
|
+
raise KeyError(
|
|
54
|
+
f"元素编号 {element_id} 不存在或已过期(页面可能已刷新,需要重新提取语义结构)"
|
|
55
|
+
)
|
|
56
|
+
return self._registry[element_id]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SemanticReducer —— HIR-Browser 的第一个核心壁垒。
|
|
3
|
+
|
|
4
|
+
问题:不能把整个 DOM 丢给 LLM(token 爆炸 + 噪音太多)。
|
|
5
|
+
方案:只保留"可交互元素 + 编号 + 简短文本",用数字 ID 代替 selector/坐标,
|
|
6
|
+
LLM 输出 click(3) 而不是猜 CSS path。
|
|
7
|
+
|
|
8
|
+
这一层不关心底层用什么 driver,只依赖 DomExtractor 和 ElementIndexer 的接口。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .snapshot import Snapshot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SemanticReducer:
|
|
15
|
+
|
|
16
|
+
def __init__(self, dom_extractor, element_indexer):
|
|
17
|
+
self.dom_extractor = dom_extractor
|
|
18
|
+
self.indexer = element_indexer
|
|
19
|
+
|
|
20
|
+
def reduce(self) -> Snapshot:
|
|
21
|
+
handles = self.dom_extractor.extract_interactive_elements()
|
|
22
|
+
|
|
23
|
+
indexed_elements = self.indexer.assign(
|
|
24
|
+
handles,
|
|
25
|
+
describe_fn=self.dom_extractor.describe_element,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return Snapshot(
|
|
29
|
+
url=self.dom_extractor.current_url(),
|
|
30
|
+
title=self.dom_extractor.current_title(),
|
|
31
|
+
elements=indexed_elements,
|
|
32
|
+
text_summary=self.dom_extractor.extract_visible_text(),
|
|
33
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
页面状态快照。
|
|
3
|
+
|
|
4
|
+
Snapshot 只存"语义化之后"的轻量结构,不存原始 DOM——
|
|
5
|
+
原始 DOM 只在 dom_extractor 里短暂存在,绝不向上层传递,
|
|
6
|
+
否则又会掉回"把整个 DOM 喂给 LLM"的老问题。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import List, Dict, Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Snapshot:
|
|
15
|
+
url: str
|
|
16
|
+
title: str
|
|
17
|
+
elements: List[Dict[str, Any]] = field(default_factory=list)
|
|
18
|
+
text_summary: str = ""
|
|
19
|
+
|
|
20
|
+
def element_ids(self) -> set:
|
|
21
|
+
return {el["id"] for el in self.elements}
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"url": self.url,
|
|
26
|
+
"title": self.title,
|
|
27
|
+
"elements": self.elements,
|
|
28
|
+
"text_summary": self.text_summary,
|
|
29
|
+
}
|