foxclaw 0.1.2 → 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/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- 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 +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -25,7 +25,6 @@ import re
|
|
|
25
25
|
from dataclasses import dataclass, field
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
# 粗略 token 估算:中文约 1 字符 ≈ 1 token,英文约 4 字符 ≈ 1 token。
|
|
29
28
|
def estimate_tokens(text: str) -> int:
|
|
30
29
|
if not text:
|
|
31
30
|
return 0
|
|
@@ -38,8 +37,6 @@ def estimate_messages_tokens(messages: list) -> int:
|
|
|
38
37
|
return sum(estimate_tokens(str(m.get("content", ""))) for m in messages)
|
|
39
38
|
|
|
40
39
|
|
|
41
|
-
# 关键标识符:不能在摘要中丢失的(ID、长数字、订单号、哈希、金额等)
|
|
42
|
-
# 不用外层 \b:¥/逗号不是单词字符,会导致金额匹配失败。
|
|
43
40
|
_IDENTIFIER_PAT = re.compile(
|
|
44
41
|
r"[a-zA-Z]{2,}_[a-zA-Z0-9]{4,}|[a-f0-9]{8,}|[0-9]{6,}|¥[\d,]+(?:\.\d+)?")
|
|
45
42
|
|
|
@@ -56,14 +53,14 @@ def extract_identifiers(messages: list) -> list[str]:
|
|
|
56
53
|
|
|
57
54
|
@dataclass
|
|
58
55
|
class CompactionResult:
|
|
59
|
-
messages: list = field(default_factory=list)
|
|
60
|
-
method: str = "none"
|
|
56
|
+
messages: list = field(default_factory=list)
|
|
57
|
+
method: str = "none"
|
|
61
58
|
original_count: int = 0
|
|
62
59
|
compacted_count: int = 0
|
|
63
60
|
original_tokens: int = 0
|
|
64
61
|
compacted_tokens: int = 0
|
|
65
62
|
preserved_identifiers: list = field(default_factory=list)
|
|
66
|
-
note: str = ""
|
|
63
|
+
note: str = ""
|
|
67
64
|
|
|
68
65
|
def to_dict(self) -> dict:
|
|
69
66
|
return {
|
|
@@ -101,8 +98,6 @@ def group_tool_calls(messages: list) -> list:
|
|
|
101
98
|
m = messages[i]
|
|
102
99
|
buf.append(m)
|
|
103
100
|
if m.get("role") == "assistant" and (m.get("tool_calls") or []):
|
|
104
|
-
# 本 assistant 声明了工具调用 → 把紧随其后、id 匹配的 tool 结果
|
|
105
|
-
# 全部并入同一组,确保「声明与结果同进同出」。
|
|
106
101
|
ids = {tc.get("id") for tc in (m.get("tool_calls") or [])}
|
|
107
102
|
j = i + 1
|
|
108
103
|
while j < n:
|
|
@@ -125,10 +120,10 @@ class Compactor:
|
|
|
125
120
|
|
|
126
121
|
def __init__(self, router=None, max_tokens: int = 6000,
|
|
127
122
|
keep_recent: int = 4, oversized_token: int = 2000):
|
|
128
|
-
self.router = router
|
|
129
|
-
self.max_tokens = max_tokens
|
|
130
|
-
self.keep_recent = keep_recent
|
|
131
|
-
self.oversized_token = oversized_token
|
|
123
|
+
self.router = router
|
|
124
|
+
self.max_tokens = max_tokens
|
|
125
|
+
self.keep_recent = keep_recent
|
|
126
|
+
self.oversized_token = oversized_token
|
|
132
127
|
|
|
133
128
|
def needs_compaction(self, messages: list) -> bool:
|
|
134
129
|
return estimate_messages_tokens(messages) > self.max_tokens
|
|
@@ -146,13 +141,9 @@ class Compactor:
|
|
|
146
141
|
res.compacted_tokens = res.original_tokens
|
|
147
142
|
return res
|
|
148
143
|
|
|
149
|
-
# 分区:系统消息 / 待压缩的早期消息 / 保留的近期消息
|
|
150
144
|
system_msgs = [m for m in messages if m.get("role") == "system"]
|
|
151
145
|
non_system = [m for m in messages if m.get("role") != "system"]
|
|
152
146
|
keep = max(0, self.keep_recent)
|
|
153
|
-
# 按**原子组**保留(而非按条数),保证 assistant.tool_calls 与其 tool 结果
|
|
154
|
-
# 同进同出。按条数切会在「一轮多工具」时把 tool 结果与其声明切散 →
|
|
155
|
-
# provider 报 400。详见 group_tool_calls docstring。
|
|
156
147
|
_groups = group_tool_calls(non_system)
|
|
157
148
|
if keep and _groups:
|
|
158
149
|
recent = [m for g in _groups[-keep:] for m in g]
|
|
@@ -161,8 +152,6 @@ class Compactor:
|
|
|
161
152
|
recent, to_compress = [], non_system
|
|
162
153
|
else:
|
|
163
154
|
recent, to_compress = [], non_system
|
|
164
|
-
# 防御:若仍不配对(极端边界/上游已产生的畸形序列),宁可放弃压缩,
|
|
165
|
-
# 也不产出非法消息序列 —— 压缩不生效只是浪费 token,发出去 400 是整轮失败。
|
|
166
155
|
_declared = {tc.get("id") for m in (recent + to_compress)
|
|
167
156
|
for tc in (m.get("tool_calls") or [])}
|
|
168
157
|
_orphans = [m for m in (recent + to_compress)
|
|
@@ -185,15 +174,12 @@ class Compactor:
|
|
|
185
174
|
|
|
186
175
|
res.preserved_identifiers = extract_identifiers(to_compress)
|
|
187
176
|
|
|
188
|
-
# —— 渐进降级 ——
|
|
189
177
|
summary = None
|
|
190
178
|
method = "truncate"
|
|
191
|
-
# 1. LLM 完整摘要
|
|
192
179
|
if self.router is not None:
|
|
193
180
|
summary = self._try_summarize(to_compress, res.preserved_identifiers)
|
|
194
181
|
if summary:
|
|
195
182
|
method = "llm_summary"
|
|
196
|
-
# 2. 部分摘要:排除超大消息后再试
|
|
197
183
|
if summary is None and self.router is not None:
|
|
198
184
|
trimmed = [m for m in to_compress
|
|
199
185
|
if estimate_tokens(str(m.get("content", ""))) <= self.oversized_token]
|
|
@@ -201,7 +187,6 @@ class Compactor:
|
|
|
201
187
|
summary = self._try_summarize(trimmed, res.preserved_identifiers)
|
|
202
188
|
if summary:
|
|
203
189
|
method = "partial_summary"
|
|
204
|
-
# 3. 截断兜底
|
|
205
190
|
if summary is None:
|
|
206
191
|
summary = self._truncate_fallback(to_compress, res.preserved_identifiers)
|
|
207
192
|
method = "truncate"
|
|
@@ -211,14 +196,11 @@ class Compactor:
|
|
|
211
196
|
compacted = system_msgs + [summary_msg] + recent
|
|
212
197
|
compacted_tokens = estimate_messages_tokens(compacted)
|
|
213
198
|
|
|
214
|
-
# 校验:压缩须真的显著变小(至少省 10%),否则放弃——避免系统消息过大时
|
|
215
|
-
# 压缩无效还白加摘要导致反而更大。
|
|
216
199
|
if compacted_tokens >= res.original_tokens * 0.9:
|
|
217
200
|
res.messages = list(messages)
|
|
218
201
|
res.method = "skipped_ineffective"
|
|
219
202
|
res.compacted_count = len(messages)
|
|
220
203
|
res.compacted_tokens = res.original_tokens
|
|
221
|
-
# 提示:压缩无效通常因系统消息/近期消息本身已占满预算
|
|
222
204
|
sys_tokens = estimate_messages_tokens(system_msgs)
|
|
223
205
|
res.note = (f"压缩跳过:有效压缩空间不足(系统消息占 {sys_tokens} token)。"
|
|
224
206
|
f"建议精简系统提示或调大上下文预算。")
|
|
@@ -242,7 +224,7 @@ class Compactor:
|
|
|
242
224
|
return list(messages), None
|
|
243
225
|
res = self.compact(messages)
|
|
244
226
|
if res.method == "none" or res.method.startswith("skipped"):
|
|
245
|
-
return list(messages), res
|
|
227
|
+
return list(messages), res
|
|
246
228
|
return res.messages, res
|
|
247
229
|
except Exception:
|
|
248
230
|
return list(messages), None
|
|
@@ -22,7 +22,7 @@ class LayeredConfig:
|
|
|
22
22
|
"""三层配置合并原语。持有各层 dict 的引用 —— 若某层的集合值是可变对象(如 SkillPolicy 的
|
|
23
23
|
user deny set),原地增删会被 merged_set/policy_locked 实时反映,无需重建。"""
|
|
24
24
|
|
|
25
|
-
def __init__(self, policy: dict = None, user: dict = None, runtime: dict = None):
|
|
25
|
+
def __init__(self, policy: dict | None = None, user: dict | None = None, runtime: dict | None = None):
|
|
26
26
|
self.policy = dict(policy or {})
|
|
27
27
|
self.user = dict(user or {})
|
|
28
28
|
self.runtime = dict(runtime or {})
|
|
@@ -61,5 +61,5 @@ def load_policy_layer(data_dir=None) -> dict:
|
|
|
61
61
|
d = json.loads(p.read_text(encoding="utf-8"))
|
|
62
62
|
deny |= set(d.get("deny") or [])
|
|
63
63
|
except Exception:
|
|
64
|
-
pass
|
|
64
|
+
pass
|
|
65
65
|
return {"deny": sorted(deny)}
|
|
@@ -25,10 +25,9 @@ import time
|
|
|
25
25
|
from dataclasses import dataclass, field
|
|
26
26
|
from typing import Optional, Tuple
|
|
27
27
|
|
|
28
|
-
#: 视为「关键操作」的副作用级别(发送/支付);destructive 无条件算关键
|
|
29
28
|
CRITICAL_SIDE_EFFECTS = ("send", "pay")
|
|
30
29
|
|
|
31
|
-
DEFAULT_TTL = 300.0
|
|
30
|
+
DEFAULT_TTL = 300.0
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
def action_fingerprint(action: str, args: dict) -> str:
|
|
@@ -78,14 +77,13 @@ class ConfirmationBroker:
|
|
|
78
77
|
self._items: dict[str, PendingConfirm] = {}
|
|
79
78
|
self._lock = threading.Lock()
|
|
80
79
|
|
|
81
|
-
# ── 签发 ────────────────────────────────────────────
|
|
82
80
|
def issue(self, session: str, action: str, args: dict,
|
|
83
|
-
ttl: float = None) -> dict:
|
|
81
|
+
ttl: float | None = None) -> dict:
|
|
84
82
|
"""签发一个绑定到 (会话, 动作, 参数) 的一次性确认码。"""
|
|
85
83
|
now = self._clock()
|
|
86
84
|
ttl = self.ttl if ttl is None else float(ttl)
|
|
87
85
|
item = PendingConfirm(
|
|
88
|
-
nonce=secrets.token_hex(8),
|
|
86
|
+
nonce=secrets.token_hex(8),
|
|
89
87
|
action=action or "",
|
|
90
88
|
fingerprint=action_fingerprint(action or "", args or {}),
|
|
91
89
|
session=session or "",
|
|
@@ -99,9 +97,8 @@ class ConfirmationBroker:
|
|
|
99
97
|
return {"nonce": item.nonce, "action": item.action,
|
|
100
98
|
"expires_at": item.expires, "ttl": ttl}
|
|
101
99
|
|
|
102
|
-
# ── 校验并消费 ──────────────────────────────────────
|
|
103
100
|
def consume(self, session: str, nonce: str, action: str = "",
|
|
104
|
-
args: dict = None, now: float = None) -> Tuple[bool, str]:
|
|
101
|
+
args: dict | None = None, now: float | None = None) -> Tuple[bool, str]:
|
|
105
102
|
"""校验确认码并**立即作废**。返回 (是否通过, 原因码)。
|
|
106
103
|
|
|
107
104
|
原因码:``ok`` / ``missing_nonce`` / ``unknown_nonce`` / ``already_used`` /
|
|
@@ -113,8 +110,6 @@ class ConfirmationBroker:
|
|
|
113
110
|
now = now if now is not None else self._clock()
|
|
114
111
|
with self._lock:
|
|
115
112
|
item: Optional[PendingConfirm] = self._items.get(nonce)
|
|
116
|
-
# 先看这一条再清理:否则过期的码会被 _purge 抹掉,排错时只会看到
|
|
117
|
-
# unknown_nonce,分不清"从没发过"和"发过但过期了"
|
|
118
113
|
if item is not None and item.expires <= now:
|
|
119
114
|
self._items.pop(nonce, None)
|
|
120
115
|
return False, "expired"
|
|
@@ -130,10 +125,9 @@ class ConfirmationBroker:
|
|
|
130
125
|
if args is not None and item.fingerprint != action_fingerprint(item.action, args):
|
|
131
126
|
return False, "args_mismatch"
|
|
132
127
|
item.used = True
|
|
133
|
-
self._items.pop(nonce, None)
|
|
128
|
+
self._items.pop(nonce, None)
|
|
134
129
|
return True, "ok"
|
|
135
130
|
|
|
136
|
-
# ── 观测 ────────────────────────────────────────────
|
|
137
131
|
def pending(self, session: str = "") -> list:
|
|
138
132
|
"""未消费且未过期的确认码(可按会话过滤),供排查/前端展示。"""
|
|
139
133
|
now = self._clock()
|
|
@@ -142,7 +136,7 @@ class ConfirmationBroker:
|
|
|
142
136
|
return [it.to_dict() for it in self._items.values()
|
|
143
137
|
if not it.used and (not session or it.session == session)]
|
|
144
138
|
|
|
145
|
-
def purge(self, now: float = None) -> int:
|
|
139
|
+
def purge(self, now: float | None = None) -> int:
|
|
146
140
|
now = now if now is not None else self._clock()
|
|
147
141
|
with self._lock:
|
|
148
142
|
return self._purge_locked(now)
|
|
@@ -37,10 +37,10 @@ def load_env_file(path: str | Path) -> List[str]:
|
|
|
37
37
|
shadows: List[str] = []
|
|
38
38
|
for key, value in (dotenv_values(target) or {}).items():
|
|
39
39
|
if not key:
|
|
40
|
-
continue
|
|
40
|
+
continue
|
|
41
41
|
value = (value or "").strip()
|
|
42
42
|
if not value:
|
|
43
|
-
continue
|
|
43
|
+
continue
|
|
44
44
|
current = os.environ.get(key, "").strip()
|
|
45
45
|
if current and current != value:
|
|
46
46
|
shadows.append(key)
|
|
@@ -16,29 +16,24 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
|
|
19
|
-
# ── 失败类型 ────────────────────────────────────────────────────────────
|
|
20
19
|
OK = "ok"
|
|
21
|
-
TIMEOUT = "timeout"
|
|
22
|
-
TRANSIENT = "transient"
|
|
23
|
-
NOT_CONFIGURED = "not_configured"
|
|
24
|
-
EMPTY = "empty"
|
|
25
|
-
INVALID_ARGS = "invalid_args"
|
|
26
|
-
DENIED = "denied"
|
|
27
|
-
NEEDS_HUMAN = "needs_human"
|
|
28
|
-
EXCEPTION = "exception"
|
|
29
|
-
UNKNOWN = "unknown"
|
|
30
|
-
|
|
31
|
-
#: 动作
|
|
20
|
+
TIMEOUT = "timeout"
|
|
21
|
+
TRANSIENT = "transient"
|
|
22
|
+
NOT_CONFIGURED = "not_configured"
|
|
23
|
+
EMPTY = "empty"
|
|
24
|
+
INVALID_ARGS = "invalid_args"
|
|
25
|
+
DENIED = "denied"
|
|
26
|
+
NEEDS_HUMAN = "needs_human"
|
|
27
|
+
EXCEPTION = "exception"
|
|
28
|
+
UNKNOWN = "unknown"
|
|
29
|
+
|
|
32
30
|
NONE = "none"
|
|
33
|
-
RETRY = "retry"
|
|
34
|
-
REPARAM = "reparam"
|
|
35
|
-
DEGRADE = "degrade"
|
|
36
|
-
HAND_TO_HUMAN = "hand_to_human"
|
|
37
|
-
STOP = "stop"
|
|
38
|
-
|
|
39
|
-
#: 特征表。**顺序敏感**:先命中先返回。顺序上的两个坑都踩过:
|
|
40
|
-
#: · "未返回结果"(empty)不能先被 "未返回"(timeout) 吃掉;
|
|
41
|
-
#: · "不合法"(invalid_args)要在 "拒绝"(denied) 之前,否则参数错误被当成越权。
|
|
31
|
+
RETRY = "retry"
|
|
32
|
+
REPARAM = "reparam"
|
|
33
|
+
DEGRADE = "degrade"
|
|
34
|
+
HAND_TO_HUMAN = "hand_to_human"
|
|
35
|
+
STOP = "stop"
|
|
36
|
+
|
|
42
37
|
_RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
43
38
|
(DENIED, ("已被策略禁用", "被权限策略拒绝", "未知工具")),
|
|
44
39
|
(NEEDS_HUMAN, ("需确认的动作", "需要你确认", "请用户确认", "只能由你亲自确认")),
|
|
@@ -50,7 +45,6 @@ _RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
|
50
45
|
(EXCEPTION, ("执行异常",)),
|
|
51
46
|
)
|
|
52
47
|
|
|
53
|
-
#: 类型 → (动作, 是否可重试, 给模型的一句硬提示)。提示写进回注文本,模型没法装作没看见。
|
|
54
48
|
_ACTIONS: dict[str, tuple[str, bool, str]] = {
|
|
55
49
|
TIMEOUT: (RETRY, True,
|
|
56
50
|
"这一步超时未完成,已自动重试一次仍有问题。请如实说明这一步没做成,不要编造结果。"),
|
|
@@ -94,9 +88,6 @@ class Decision:
|
|
|
94
88
|
"retryable": self.retryable, "hint": self.hint}
|
|
95
89
|
|
|
96
90
|
|
|
97
|
-
#: 失败回注的形态标记。工具执行侧的失败文本统一以 "(" 开头(见 tool_exec 的兜底),
|
|
98
|
-
#: 另有少数异常文本不带括号。**不能**用"含 未/失败 就算失败"这种宽松判据:
|
|
99
|
-
#: 搜索正文里"未来""失败案例"都会误伤,把成功结果标成失败。
|
|
100
91
|
_FAILURE_MARKERS = ("执行异常", "入参不合法", "被权限策略拒绝", "缺少必填")
|
|
101
92
|
|
|
102
93
|
|
|
@@ -111,7 +102,7 @@ def classify(text: str) -> str:
|
|
|
111
102
|
"""把回注文本归类:``ok`` / 具体失败类型 / ``unknown``(认不出来,保守交人)。"""
|
|
112
103
|
t = (text or "").strip()
|
|
113
104
|
if not t:
|
|
114
|
-
return UNKNOWN
|
|
105
|
+
return UNKNOWN
|
|
115
106
|
if not looks_like_failure(t):
|
|
116
107
|
return OK
|
|
117
108
|
low = t.lower()
|
|
@@ -19,15 +19,15 @@ class Gateway:
|
|
|
19
19
|
def __init__(self, rate_per_min: int = 600, quota_per_hour: int = 20000,
|
|
20
20
|
max_clients: int = 4096):
|
|
21
21
|
self.rate = max(1, int(rate_per_min))
|
|
22
|
-
self.capacity = float(self.rate)
|
|
23
|
-
self.refill = self.capacity / 60.0
|
|
22
|
+
self.capacity = float(self.rate)
|
|
23
|
+
self.refill = self.capacity / 60.0
|
|
24
24
|
self.quota = max(1, int(quota_per_hour))
|
|
25
25
|
self.max_clients = max(64, int(max_clients))
|
|
26
26
|
self._lock = threading.Lock()
|
|
27
|
-
self._buckets: dict = {}
|
|
28
|
-
self._quota: dict = {}
|
|
29
|
-
self._seen: dict = {}
|
|
30
|
-
self.rejected = 0
|
|
27
|
+
self._buckets: dict = {}
|
|
28
|
+
self._quota: dict = {}
|
|
29
|
+
self._seen: dict = {}
|
|
30
|
+
self.rejected = 0
|
|
31
31
|
|
|
32
32
|
def check(self, client: str):
|
|
33
33
|
"""判定一次请求。返回 (allowed: bool, retry_after: int, reason: str)。
|
|
@@ -37,7 +37,6 @@ class Gateway:
|
|
|
37
37
|
self._evict_locked(now)
|
|
38
38
|
self._seen[client] = now
|
|
39
39
|
|
|
40
|
-
# ① 配额(固定 1 小时窗口)
|
|
41
40
|
q = self._quota.get(client)
|
|
42
41
|
if q is None or now - q[1] >= 3600.0:
|
|
43
42
|
q = [0, now]
|
|
@@ -46,7 +45,6 @@ class Gateway:
|
|
|
46
45
|
self.rejected += 1
|
|
47
46
|
return False, int(3600.0 - (now - q[1])) + 1, "quota"
|
|
48
47
|
|
|
49
|
-
# ② 令牌桶(突发限流)
|
|
50
48
|
b = self._buckets.get(client)
|
|
51
49
|
if b is None:
|
|
52
50
|
b = [self.capacity, now]
|
|
@@ -57,7 +55,6 @@ class Gateway:
|
|
|
57
55
|
self.rejected += 1
|
|
58
56
|
return False, int((1.0 - b[0]) / self.refill) + 1, "rate"
|
|
59
57
|
|
|
60
|
-
# 放行:扣 1 令牌 + 计 1 配额
|
|
61
58
|
b[0] -= 1.0
|
|
62
59
|
q[0] += 1
|
|
63
60
|
return True, 0, ""
|
|
@@ -67,7 +64,6 @@ class Gateway:
|
|
|
67
64
|
if len(self._seen) <= self.max_clients:
|
|
68
65
|
return
|
|
69
66
|
n_drop = len(self._seen) - self.max_clients + max(1, self.max_clients // 10)
|
|
70
|
-
# 按 last_ts 升序取最旧的 n_drop 个
|
|
71
67
|
victims = sorted(self._seen.items(), key=lambda kv: kv[1])[:n_drop]
|
|
72
68
|
for c, _ in victims:
|
|
73
69
|
self._seen.pop(c, None)
|
|
@@ -46,11 +46,9 @@ from . import audit_chain
|
|
|
46
46
|
ENV_SWITCH = "FOXCLAW_GOVERNANCE_AUDIT"
|
|
47
47
|
TOPIC = "governance.decision"
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
VERDICTS_EXECUTION = ("success", "failed") # 执行类
|
|
49
|
+
VERDICTS_RULING = ("allow", "warn", "deny", "confirm")
|
|
50
|
+
VERDICTS_EXECUTION = ("success", "failed")
|
|
52
51
|
|
|
53
|
-
# 已知层名(同上,仅文档)
|
|
54
52
|
LAYERS = ("harness", "permission", "orchestration", "skill")
|
|
55
53
|
|
|
56
54
|
|
|
@@ -71,7 +69,6 @@ class GovernanceAuditor:
|
|
|
71
69
|
self._spine = spine
|
|
72
70
|
self._path = Path(sink_path) if sink_path else None
|
|
73
71
|
self._lock = threading.Lock()
|
|
74
|
-
# 链尾 hash:首次落盘时从文件尾部补齐(重启后接着上一条链,而不是重开 GENESIS)
|
|
75
72
|
self._last_hash: str | None = None
|
|
76
73
|
|
|
77
74
|
@property
|
|
@@ -95,7 +92,7 @@ class GovernanceAuditor:
|
|
|
95
92
|
"actor": actor,
|
|
96
93
|
"action": action,
|
|
97
94
|
"verdict": verdict,
|
|
98
|
-
"reason": (reason or "")[:200],
|
|
95
|
+
"reason": (reason or "")[:200],
|
|
99
96
|
"detail": detail or {},
|
|
100
97
|
"ts": datetime.now(timezone.utc).isoformat(),
|
|
101
98
|
}
|
|
@@ -103,7 +100,6 @@ class GovernanceAuditor:
|
|
|
103
100
|
self._persist(rec)
|
|
104
101
|
return did
|
|
105
102
|
|
|
106
|
-
# ── 内部:两个通道,各自独立 fail-safe ─────────────────────
|
|
107
103
|
|
|
108
104
|
def _emit(self, rec: dict) -> None:
|
|
109
105
|
if self._spine is None:
|
|
@@ -27,7 +27,7 @@ from dataclasses import dataclass, field
|
|
|
27
27
|
class _Record:
|
|
28
28
|
key: str
|
|
29
29
|
created: float
|
|
30
|
-
result: dict = field(default_factory=dict)
|
|
30
|
+
result: dict = field(default_factory=dict)
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def make_key(domain: str, *parts: str, version: str = "v1") -> str:
|
|
@@ -52,16 +52,14 @@ class IdempotencyGuard:
|
|
|
52
52
|
"""
|
|
53
53
|
|
|
54
54
|
def __init__(self, ttl_seconds: float = 3600, max_size: int = 10000,
|
|
55
|
-
persist_path: str = None, spine=None,
|
|
55
|
+
persist_path: str | None = None, spine=None,
|
|
56
56
|
compact_every: int = 1000):
|
|
57
57
|
self.ttl = ttl_seconds
|
|
58
58
|
self.max_size = max_size
|
|
59
59
|
self.persist_path = persist_path
|
|
60
60
|
self._store: dict[str, _Record] = {}
|
|
61
61
|
self._lock = threading.RLock()
|
|
62
|
-
# 【P2-3】审计脊柱(可空,None 时静默)
|
|
63
62
|
self._spine = spine
|
|
64
|
-
# 【P2-3】追加日志 + 定期 compact
|
|
65
63
|
self._log_path = (persist_path + ".log") if persist_path else None
|
|
66
64
|
self._compact_every = compact_every
|
|
67
65
|
self._ops_since_compact = 0
|
|
@@ -82,7 +80,7 @@ class IdempotencyGuard:
|
|
|
82
80
|
rec = self._store.get(key)
|
|
83
81
|
return dict(rec.result) if rec else None
|
|
84
82
|
|
|
85
|
-
def mark(self, key: str, result: dict = None):
|
|
83
|
+
def mark(self, key: str, result: dict | None = None):
|
|
86
84
|
"""标记 key 已执行,记录结果。"""
|
|
87
85
|
with self._lock:
|
|
88
86
|
self._store[key] = _Record(key=key, created=time.time(),
|
|
@@ -92,9 +90,9 @@ class IdempotencyGuard:
|
|
|
92
90
|
result=self._store[key].result)
|
|
93
91
|
need_compact = self._ops_since_compact >= self._compact_every
|
|
94
92
|
if need_compact:
|
|
95
|
-
self._try_compact()
|
|
93
|
+
self._try_compact()
|
|
96
94
|
|
|
97
|
-
def check_and_mark(self, key: str, result: dict = None) -> bool:
|
|
95
|
+
def check_and_mark(self, key: str, result: dict | None = None) -> bool:
|
|
98
96
|
"""原子操作:若 key 未见过 → 标记并返回 True(应执行);
|
|
99
97
|
若已见过 → 返回 False(应跳过)。
|
|
100
98
|
|
|
@@ -104,7 +102,7 @@ class IdempotencyGuard:
|
|
|
104
102
|
with self._lock:
|
|
105
103
|
self._evict_expired()
|
|
106
104
|
if key in self._store:
|
|
107
|
-
return False
|
|
105
|
+
return False
|
|
108
106
|
self._store[key] = _Record(key=key, created=time.time(),
|
|
109
107
|
result=dict(result or {}))
|
|
110
108
|
self._enforce_capacity()
|
|
@@ -128,7 +126,6 @@ class IdempotencyGuard:
|
|
|
128
126
|
if need_compact:
|
|
129
127
|
self._try_compact()
|
|
130
128
|
|
|
131
|
-
# ── 内部:过期/容量/持久化 ──────────────────────────────
|
|
132
129
|
def _evict_expired(self):
|
|
133
130
|
if self.ttl <= 0:
|
|
134
131
|
return
|
|
@@ -138,9 +135,6 @@ class IdempotencyGuard:
|
|
|
138
135
|
del self._store[k]
|
|
139
136
|
|
|
140
137
|
def _enforce_capacity(self):
|
|
141
|
-
# 超容量 → 淘汰最旧的(按 created)。
|
|
142
|
-
# 【P2-3】淘汰必须落 delete 日志:否则重启回放追加日志时,被淘汰的 key
|
|
143
|
-
# 会"复活"(mark 日志还在,没有对应的删除记录)。锁内调用。
|
|
144
138
|
if len(self._store) <= self.max_size:
|
|
145
139
|
return
|
|
146
140
|
items = sorted(self._store.items(), key=lambda kv: kv[1].created)
|
|
@@ -148,7 +142,6 @@ class IdempotencyGuard:
|
|
|
148
142
|
del self._store[k]
|
|
149
143
|
self._persist_op("delete", k)
|
|
150
144
|
|
|
151
|
-
# ── 内部:持久化(追加日志 + 定期 compact)─────────────────
|
|
152
145
|
def _persist_op(self, event: str, key: str, **fields):
|
|
153
146
|
"""【P2-3】追加一条日志(O(1) 写,不写全量)。锁内调用(append 很快,
|
|
154
147
|
且在锁内可避免并发写同一文件交错)。失败计数 + 发事件,不影响主流程。"""
|
|
@@ -196,11 +189,10 @@ class IdempotencyGuard:
|
|
|
196
189
|
for k, r in self._store.items()
|
|
197
190
|
if self.ttl <= 0 or now - r.created <= self.ttl}
|
|
198
191
|
self._ops_since_compact = 0
|
|
199
|
-
# ── 以下为主锁外的慢 IO ──
|
|
200
192
|
tmp = self.persist_path + ".tmp"
|
|
201
193
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
202
194
|
json.dump(snapshot, f, ensure_ascii=False)
|
|
203
|
-
os.replace(tmp, self.persist_path)
|
|
195
|
+
os.replace(tmp, self.persist_path)
|
|
204
196
|
if rotated and os.path.exists(rotated):
|
|
205
197
|
os.remove(rotated)
|
|
206
198
|
except Exception:
|
|
@@ -222,7 +214,7 @@ class IdempotencyGuard:
|
|
|
222
214
|
tmp = self.persist_path + ".tmp"
|
|
223
215
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
224
216
|
json.dump(data, f, ensure_ascii=False)
|
|
225
|
-
os.replace(tmp, self.persist_path)
|
|
217
|
+
os.replace(tmp, self.persist_path)
|
|
226
218
|
if self._log_path and os.path.exists(self._log_path):
|
|
227
219
|
os.remove(self._log_path)
|
|
228
220
|
self._ops_since_compact = 0
|
|
@@ -254,8 +246,9 @@ class IdempotencyGuard:
|
|
|
254
246
|
一份全量快照,失去"追加日志 O(1)"的意义。"""
|
|
255
247
|
try:
|
|
256
248
|
now = time.time()
|
|
257
|
-
|
|
258
|
-
|
|
249
|
+
snapshot = self.persist_path
|
|
250
|
+
if snapshot is not None and os.path.exists(snapshot):
|
|
251
|
+
with open(snapshot, encoding="utf-8") as f:
|
|
259
252
|
data = json.load(f)
|
|
260
253
|
for k, v in data.items():
|
|
261
254
|
created = v.get("created", now)
|
|
@@ -272,18 +265,18 @@ class IdempotencyGuard:
|
|
|
272
265
|
try:
|
|
273
266
|
ev = json.loads(line)
|
|
274
267
|
except Exception:
|
|
275
|
-
continue
|
|
268
|
+
continue
|
|
276
269
|
self._apply_log_event(ev, now)
|
|
277
270
|
replayed = True
|
|
278
271
|
if replayed:
|
|
279
272
|
self._compact()
|
|
280
273
|
except Exception:
|
|
281
|
-
pass
|
|
274
|
+
pass
|
|
282
275
|
|
|
283
276
|
def stats(self) -> dict:
|
|
284
277
|
with self._lock:
|
|
285
278
|
self._evict_expired()
|
|
286
279
|
return {"size": len(self._store), "max_size": self.max_size,
|
|
287
280
|
"ttl_seconds": self.ttl, "persisted": bool(self.persist_path),
|
|
288
|
-
"persist_failures": self._persist_failures,
|
|
289
|
-
"ops_since_compact": self._ops_since_compact}
|
|
281
|
+
"persist_failures": self._persist_failures,
|
|
282
|
+
"ops_since_compact": self._ops_since_compact}
|
|
@@ -18,7 +18,6 @@ import os
|
|
|
18
18
|
import re
|
|
19
19
|
import secrets
|
|
20
20
|
|
|
21
|
-
# 典型注入/越权特征(中英)。命中不拦截,只在数据块里加醒目告警,提示模型"这是数据里的指令,别执行"。
|
|
22
21
|
_MARKERS = [
|
|
23
22
|
r"ignore\s+(all\s+)?(the\s+)?previous\s+instructions?",
|
|
24
23
|
r"disregard\s+(the\s+)?(above|previous|prior)",
|
|
@@ -42,25 +41,17 @@ def scan(text: str) -> bool:
|
|
|
42
41
|
return bool(text) and bool(_RE.search(str(text)))
|
|
43
42
|
|
|
44
43
|
|
|
45
|
-
# ── 输出侧护栏(纵深防御第四道:DLP)────────────────────────────────────────────
|
|
46
|
-
# 回复返回/落库前扫一道:① 密钥/令牌等敏感串 → 脱敏;② 系统提示词被整段吐出 → 拦截。
|
|
47
|
-
# 纯正则、零额外模型调用(适配 flash 成本)。与"输入-隔离-模型"三层凑齐四道墙。
|
|
48
|
-
|
|
49
|
-
# 常见密钥/令牌形态(含 Foxclaw 实际用的百度千帆 bce-v3 AppBuilder key)。
|
|
50
44
|
_SECRET_PATTERNS = [
|
|
51
|
-
r"sk-[A-Za-z0-9]{20,}",
|
|
52
|
-
r"bce-v3/[A-Za-z0-9-]{6,}/[A-Za-z0-9]{6,}",
|
|
53
|
-
r"AKIA[0-9A-Z]{16}",
|
|
54
|
-
r"gh[pousr]_[A-Za-z0-9]{20,}",
|
|
55
|
-
r"xox[baprs]-[A-Za-z0-9-]{10,}",
|
|
56
|
-
r"Bearer\s+[A-Za-z0-9._\-]{20,}",
|
|
45
|
+
r"sk-[A-Za-z0-9]{20,}",
|
|
46
|
+
r"bce-v3/[A-Za-z0-9-]{6,}/[A-Za-z0-9]{6,}",
|
|
47
|
+
r"AKIA[0-9A-Z]{16}",
|
|
48
|
+
r"gh[pousr]_[A-Za-z0-9]{20,}",
|
|
49
|
+
r"xox[baprs]-[A-Za-z0-9-]{10,}",
|
|
50
|
+
r"Bearer\s+[A-Za-z0-9._\-]{20,}",
|
|
57
51
|
r"(api[_-]?key|secret|token|password|passwd)\s*[:=]\s*['\"]?[A-Za-z0-9._\-/]{12,}",
|
|
58
52
|
]
|
|
59
53
|
_SECRET_RE = re.compile("|".join(_SECRET_PATTERNS), re.IGNORECASE)
|
|
60
54
|
|
|
61
|
-
# 系统提示词泄露判定:不用"单个短语命中"(会误伤——如用户正常问"Principle 07 是什么")。
|
|
62
|
-
# 改为**更强信号**:① 输出里出现 KERNEL 的长段**逐字串**(dump 才会逐字复现,提及概念不会);
|
|
63
|
-
# ② 或同时出现 ≥2 个**章节标记**(无 kernel 参照时的兜底)。
|
|
64
55
|
_SECTION_MARKS = ("【一、", "【二、", "【三、", "【四、", "【五、", "【六、", "【七、")
|
|
65
56
|
|
|
66
57
|
|
|
@@ -101,7 +92,6 @@ def _has_semantic_leak(text: str, kernel: str, threshold: float = 0.88,
|
|
|
101
92
|
if model is None:
|
|
102
93
|
return False
|
|
103
94
|
import numpy as np
|
|
104
|
-
# 固定 ~200 字块;向量已 normalize → 余弦 = 点积。内核块进共享缓存(只编码一次)。
|
|
105
95
|
kblocks = [kernel[i:i + 200] for i in range(0, len(kernel), 200)]
|
|
106
96
|
tblocks = [text[i:i + 200] for i in range(0, len(text), 200)]
|
|
107
97
|
kv = encode_cached(model, name, kblocks)
|
|
@@ -165,8 +155,6 @@ def guard_output(text: str, kernel: str = "") -> tuple:
|
|
|
165
155
|
return (text, issues)
|
|
166
156
|
|
|
167
157
|
|
|
168
|
-
# 分隔符伪造防护:外部内容里若自带 <untrusted>/</untrusted>,会伪造边界把恶意指令挤出隔离区
|
|
169
|
-
# (经典分隔符注入)。包裹前把内容中一切 <(/)untrusted 的尖括号中和成 ‹,使其无法充当真边界。
|
|
170
158
|
_UNTRUSTED_DELIM = re.compile(r"<\s*/?\s*untrusted", re.IGNORECASE)
|
|
171
159
|
|
|
172
160
|
|
|
@@ -196,17 +184,10 @@ def wrap_untrusted(text: str, source: str = "external") -> str:
|
|
|
196
184
|
return block + note
|
|
197
185
|
|
|
198
186
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
# 传播与裁决在调用方(agents.base / skills.permission)。
|
|
204
|
-
#
|
|
205
|
-
# 敏感度分级(数值越高越敏感,单调可比):
|
|
206
|
-
TAINT_NONE = 0 # 无外部数据(会话初始态)
|
|
207
|
-
TAINT_EXTERNAL = 1 # 外部不可控但非个人:web_search 结果
|
|
208
|
-
TAINT_PERSONAL = 2 # 用户个人数据:知识库(knowledge)/文件(file)
|
|
209
|
-
TAINT_SENSITIVE = 3 # 高敏感个人数据:个人记忆(memory)
|
|
187
|
+
TAINT_NONE = 0
|
|
188
|
+
TAINT_EXTERNAL = 1
|
|
189
|
+
TAINT_PERSONAL = 2
|
|
190
|
+
TAINT_SENSITIVE = 3
|
|
210
191
|
|
|
211
192
|
_TAINT_BY_SOURCE = {
|
|
212
193
|
"web_search": TAINT_EXTERNAL,
|