sliceagent 0.1.7__tar.gz → 0.1.9__tar.gz
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.
- {sliceagent-0.1.7 → sliceagent-0.1.9}/CHANGELOG.md +46 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/PKG-INFO +1 -1
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/__init__.py +1 -1
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/cli.py +56 -15
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/config.py +6 -1
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/llm.py +13 -3
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/neocortex.py +4 -2
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/onboarding.py +57 -7
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/policy.py +28 -4
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/tools.py +9 -3
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/tui.py +9 -2
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bughunt_fixes.py +78 -1
- sliceagent-0.1.9/tests/test_config_journey.py +260 -0
- sliceagent-0.1.7/tests/test_config_journey.py +0 -119
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.dockerignore +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.env.example +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.github/dependabot.yml +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.github/workflows/ci.yml +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.github/workflows/publish.yml +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/.gitignore +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/CODE_OF_CONDUCT.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/CONTRIBUTING.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/Dockerfile +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/LICENSE +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/NOTICE +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/QUICKSTART.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/README.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/SECURITY.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/examples/plugins/hello/__init__.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/examples/plugins/hello/plugin.toml +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/install.sh +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/pyproject.toml +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/scripts/gen_config_reference.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/scripts/run_tests.sh +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/sliceagent.toml.example +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/__main__.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/access.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/agents.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/background_review.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/binsniff.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/clock.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/code_grep.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/code_index.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/context_overflow.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/envspec.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/errors.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/events.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/finding_types.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/flags.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/fuzzy.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/guardrails.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/guidance.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/hippocampus.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/hooks.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/interfaces.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/loop.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/mcp_client.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/mcp_security.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/memory.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/metrics.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/model_catalog.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/monitor.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/oracle.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/pagetable.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/pfc.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/plugins.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/procman.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/prompt.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/records.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/recovery.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/regions.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/registry.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/retriever.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/safety.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/sandbox.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/scheduler.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/search_index.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/seed.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/sensory_cortex.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/session.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/skill_provenance.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/skill_usage.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/skills.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/subagent.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/subdir_hints.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/swap.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/taskstate.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/telemetry.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/terminal.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/text_utils.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/tool_summary.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/src/sliceagent/web.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/README.md +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_active_focus.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_agents.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_ask_echo.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_ask_user.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_background_review.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_binary_view.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bound_is_relevance.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_breadth_e2e.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_2b.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_breadth_wave.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_current_request.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_linenum.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_loop_wave.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_memory_wave.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_p0a.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_p0b.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_p0c.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_selfreview.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_selfreview_siblings.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_skills_h1.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_slice_wave.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_subagent_wave.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_bugfix_tools_wave.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_cache_manifest.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_checkpoint.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_cli_smoke.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_closure.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_code_grep.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_code_index.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_consolidate.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_context_overflow.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_conversation.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_coresidency.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_echo_before_blocking.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_episode.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_errors_backoff.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_esc_sentinel.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_exec_env.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_finding_types.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_flags.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_fuzzy.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_ghost_index.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_guardrails.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_guidance.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_history.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_invariant_fixes.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_live_composer.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_llm_streaming.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_llm_usage_cache.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_llm_watchdog.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_log_redaction.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_loop_overflow.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_mcp_output_cap.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_mcp_runtime.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_memory.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_memory_persist.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_menu_select.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_metrics.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_mining.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_mission_tier.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_model_catalog.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_monitor.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_onboarding.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_pageout_compaction.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_permission_patterns.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_pinned_composer.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_plan_tier.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_procman.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_product_features.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_progress_status.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_prompt_ab.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_provenance.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_provider_lineup.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_proxy_interrupt.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_reach.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_readonly_subagent.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_recall_search.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_records.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_refault.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_registry_validation.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_reliability.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_repo_map_bounded.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_requirements.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_review_fixes.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_route_lexical.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_safety.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_seal_markdown.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_search_index.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_skill_meta.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_skill_metadata.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_subdir_hints.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_task_agnostic.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_task_state_roundtrip.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_telemetry_convergence.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_terminal.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_tool_dedup.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_tool_result_ok.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_tools_robust.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_topic_switch.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_trajectory.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_tui.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_tui_menus.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_tui_render.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_tui_widgets.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_turn_budget.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_verification_agent.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_web.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_workspace.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_world_region.py +0 -0
- {sliceagent-0.1.7 → sliceagent-0.1.9}/tests/test_world_state.py +0 -0
|
@@ -5,6 +5,52 @@ this project aims for [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.9] — 2026-07-03
|
|
9
|
+
|
|
10
|
+
Bug-hunt round 2 (deep-core lenses, full 3-vote adversarial verify). Five confirmed fixes.
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- The durable debug log's `_scrub_args` now redacts secrets in NESTED dict/list tool arguments
|
|
14
|
+
(e.g. an MCP call's `{config:{api_key:…}}` or `{headers:{Authorization:…}}`) — top-level-only
|
|
15
|
+
redaction leaked them to `~/.sliceagent/logs/**/durable-log.jsonl` in plaintext.
|
|
16
|
+
- `AGENT_PROXY=none` (documented "force a DIRECT connection") now truly forces direct: the httpx
|
|
17
|
+
client is built with `trust_env=False`, so an ambient `HTTPS_PROXY` can no longer silently route
|
|
18
|
+
your API traffic through a proxy you told sliceagent to bypass (both first build and `/model` hops).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `str_replace`/`edit_file` no longer flip a whole file to CRLF when it's mostly-LF but contains a
|
|
22
|
+
single embedded `\r\n` (a byte literal, an HTTP fixture, a merge artifact). CRLF is now detected by
|
|
23
|
+
DOMINANCE, not mere presence — uniformly-CRLF Windows files are still preserved.
|
|
24
|
+
- Session-end consolidation no longer crashes (and silently discards ALL of a session's promoted
|
|
25
|
+
lessons/skills) when the episodic log contains one malformed record.
|
|
26
|
+
- `/undo` and `/cwd` (and `/plugins`) no longer corrupt output or crash Rich with a MarkupError when a
|
|
27
|
+
path contains brackets — e.g. a Next.js `app/[id]/page.tsx` route or `~/proj/[slug]`.
|
|
28
|
+
|
|
29
|
+
## [0.1.8] — 2026-07-02
|
|
30
|
+
|
|
31
|
+
Launch-day bug-hunt round: a security fix plus config-robustness and /model correctness.
|
|
32
|
+
|
|
33
|
+
### Security
|
|
34
|
+
- `run_command` in the default *teenager* mode no longer auto-runs exec/write commands disguised as
|
|
35
|
+
"read-only": `env <program>`, `sort -o FILE`, `date -s`, `tree -o FILE`, `uniq IN OUT`, and
|
|
36
|
+
`git grep --open-files-in-pager` now take the confirm path (arbitrary code-exec / file-overwrite
|
|
37
|
+
confirm-bypass). Read-only siblings like `du -s` / `grep -o` still auto-run.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- Boot no longer cross-wires a prefs-pinned provider's key with the DEFAULT provider's endpoint (an
|
|
41
|
+
env `gpt-5.5` @ OpenAI pin on a DeepSeek-default config used to 401 every call on relaunch).
|
|
42
|
+
- A prefs `provider`/model pin whose `[providers.<id>]` table was removed from config.toml is now
|
|
43
|
+
dropped at boot instead of forcing a model onto the wrong endpoint.
|
|
44
|
+
- The wizard no longer silently ERASES other providers' API keys when config.toml is unparseable —
|
|
45
|
+
the old file is moved to a non-clobbering `.bak` first (keys recoverable).
|
|
46
|
+
- `sliceagent config` / `config --use` / the init wizard no longer crash on a non-UTF-8 config.toml
|
|
47
|
+
or a scalar value under `[providers]` (they degrade).
|
|
48
|
+
- `llm.switch()` (a `/model` provider hop) closes the replaced HTTP client — no more fd leak per hop.
|
|
49
|
+
- The `/model` reasoning menu offers `high` (not a misleading `max`) for OpenRouter models, and offers
|
|
50
|
+
the levels the *target* provider supports when the pick rebinds the endpoint.
|
|
51
|
+
- The wizard's typed provider fallback rejects an unknown choice instead of silently configuring
|
|
52
|
+
OpenRouter.
|
|
53
|
+
|
|
8
54
|
## [0.1.7] — 2026-07-02
|
|
9
55
|
|
|
10
56
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sliceagent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: A coding agent with a new context-engineering framework: bounded, deterministic, reconstructed context (slice architecture) — built for long-horizon work.
|
|
5
5
|
Author: TT-Wang
|
|
6
6
|
License-Expression: MIT
|
|
@@ -50,7 +50,15 @@ def log_sink(root: str = ".", path: str | None = None):
|
|
|
50
50
|
path = path or os.path.join(state_dir("logs", root_key(root)), "durable-log.jsonl")
|
|
51
51
|
|
|
52
52
|
def _scrub_args(args: dict) -> dict: # redact string values (edit_file content, inline tokens)
|
|
53
|
-
|
|
53
|
+
def _rec(v): # RECURSE — a secret nested in a dict/list arg (e.g. an
|
|
54
|
+
if isinstance(v, str): # MCP tool's {config:{api_key:…}} / {headers:{Authorization:…}})
|
|
55
|
+
return redact_text(v) # must not reach the on-disk log in plaintext (top-level-only
|
|
56
|
+
if isinstance(v, dict): # redaction leaked it; sibling hippocampus._clamp already recurses)
|
|
57
|
+
return {k: _rec(x) for k, x in v.items()}
|
|
58
|
+
if isinstance(v, (list, tuple)):
|
|
59
|
+
return [_rec(x) for x in v]
|
|
60
|
+
return v
|
|
61
|
+
return _rec(args or {})
|
|
54
62
|
|
|
55
63
|
def sink(e: Event) -> None:
|
|
56
64
|
rec = None
|
|
@@ -151,6 +159,21 @@ def _ws_name(path: str) -> str:
|
|
|
151
159
|
return "~" if p == os.path.realpath(os.path.expanduser("~")) else (os.path.basename(p) or p)
|
|
152
160
|
|
|
153
161
|
|
|
162
|
+
def _env_from_config(c, pid: str | None = None) -> None:
|
|
163
|
+
"""Populate LLM_API_KEY/LLM_BASE_URL from config (ENV still wins). A prefs-pinned provider `pid`
|
|
164
|
+
binds AS A UNIT: its key with ITS OWN endpoint — an absent base_url means the SDK default (OpenAI),
|
|
165
|
+
NOT the default provider's base_url (that fallback cross-wired an openai key onto the deepseek
|
|
166
|
+
endpoint after `/model`-hop + restart when default_provider pointed elsewhere)."""
|
|
167
|
+
tbl = (c.providers() or {}).get(pid) if pid else None
|
|
168
|
+
if isinstance(tbl, dict) and tbl.get("api_key"):
|
|
169
|
+
key, base = tbl["api_key"], tbl.get("base_url") or ""
|
|
170
|
+
else: # no/unusable pin → the default provider's own pairing
|
|
171
|
+
key, base = c.api_key, c.base_url
|
|
172
|
+
for _env, _val in (("LLM_API_KEY", key), ("LLM_BASE_URL", base)):
|
|
173
|
+
if not os.environ.get(_env) and _val:
|
|
174
|
+
os.environ[_env] = _val
|
|
175
|
+
|
|
176
|
+
|
|
154
177
|
def main() -> None:
|
|
155
178
|
# subcommands (onboarding / discovery) are handled BEFORE any key gate, so `sliceagent init` runs on a
|
|
156
179
|
# machine with nothing configured yet. A bare `sliceagent` (or one with non-subcommand args) falls through.
|
|
@@ -165,14 +188,6 @@ def main() -> None:
|
|
|
165
188
|
from .config import load_config, load_prefs
|
|
166
189
|
_boot_prefs = load_prefs() # the last /model choice may pin a PROVIDER (endpoint+key), not just a model
|
|
167
190
|
|
|
168
|
-
def _env_from_config(c, pid: str | None = None) -> None:
|
|
169
|
-
tbl = (c.providers() or {}).get(pid) if pid else None
|
|
170
|
-
key = (tbl or {}).get("api_key") or c.api_key
|
|
171
|
-
base = ((tbl or {}).get("base_url") if tbl and tbl.get("api_key") else None) or c.base_url
|
|
172
|
-
for _env, _val in (("LLM_API_KEY", key), ("LLM_BASE_URL", base)):
|
|
173
|
-
if not os.environ.get(_env) and _val:
|
|
174
|
-
os.environ[_env] = _val
|
|
175
|
-
|
|
176
191
|
def _key_present() -> bool:
|
|
177
192
|
return bool(os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
|
178
193
|
or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("DEEPSEEK_API_KEY")
|
|
@@ -189,6 +204,14 @@ def main() -> None:
|
|
|
189
204
|
return run_init() == 0
|
|
190
205
|
|
|
191
206
|
cfg = load_config()
|
|
207
|
+
# A prefs `provider` pin whose [providers.<id>] table was since removed from config.toml is STALE:
|
|
208
|
+
# its key/endpoint already fall back to the default provider (see _env_from_config), but its pinned
|
|
209
|
+
# MODEL would still win at the resolution below and get sent to the wrong endpoint (model_not_found
|
|
210
|
+
# every turn). Drop the whole stale pin so model/endpoint resolve together from the live config.
|
|
211
|
+
if _boot_prefs.get("provider") and _boot_prefs["provider"] not in (cfg.providers() or {}):
|
|
212
|
+
from .config import save_prefs as _sp
|
|
213
|
+
_sp({"provider": None, "model": None}) # None DELETES (see save_prefs)
|
|
214
|
+
_boot_prefs.pop("provider", None); _boot_prefs.pop("model", None)
|
|
192
215
|
_env_from_config(cfg, _boot_prefs.get("provider"))
|
|
193
216
|
if not _key_present() and _first_run_setup("Welcome! No API key configured yet"):
|
|
194
217
|
cfg = load_config() # the wizard just wrote ~/.sliceagent/config.toml — pick it up
|
|
@@ -468,6 +491,18 @@ def main() -> None:
|
|
|
468
491
|
ans = input(f" ⚠ allow {name} {str(detail)[:60]!r}? ({reason}) [y]es/[n]o/[a]lways: ").strip().lower()
|
|
469
492
|
return {"y": "yes", "yes": "yes", "a": "always", "always": "always"}.get(ans, "no")
|
|
470
493
|
|
|
494
|
+
def _live_pid():
|
|
495
|
+
"""The configured provider the LIVE llm is actually bound to (endpoint+key match), or None.
|
|
496
|
+
Prefs must pin what's IN EFFECT, not the last menu pick — a typed `/model <name>` keeps the
|
|
497
|
+
endpoint, so blindly re-saving the old pin resurrected a dead endpoint+model pairing at boot."""
|
|
498
|
+
lbase = getattr(llm, "_base_url", "") or ""
|
|
499
|
+
lkey = getattr(llm.client, "api_key", None)
|
|
500
|
+
for p, t in (cfg.providers() or {}).items():
|
|
501
|
+
if isinstance(t, dict) and t.get("api_key") and t["api_key"] == lkey \
|
|
502
|
+
and (t.get("base_url") or "") == lbase:
|
|
503
|
+
return p
|
|
504
|
+
return None
|
|
505
|
+
|
|
471
506
|
def _handle_slash(line): # TUI navigation palette — wired to existing session ops
|
|
472
507
|
nonlocal cfg # /config hot-reloads the config after the in-session wizard
|
|
473
508
|
parts = line.split(maxsplit=1)
|
|
@@ -539,12 +574,15 @@ def main() -> None:
|
|
|
539
574
|
except Exception:
|
|
540
575
|
_console.print(f" no such topic: {arg}", markup=False)
|
|
541
576
|
elif cmd == "/undo":
|
|
542
|
-
|
|
577
|
+
# markup=False: undo_last() embeds the edited file PATH; a Next.js-style '[id]'/'[...slug]'
|
|
578
|
+
# segment is parsed as a Rich tag → corrupted output or a MarkupError crash.
|
|
579
|
+
_console.print(" " + base_tools.undo_last(), markup=False) # revert the last file edit
|
|
543
580
|
elif cmd == "/plugins":
|
|
544
581
|
tools = sorted(e.name for e in base_tools.registry._tools.values()
|
|
545
582
|
if getattr(e, "source", "") == "plugin")
|
|
546
|
-
|
|
547
|
-
_console.print(f" plugin
|
|
583
|
+
# markup=False: plugin dirs are filesystem PATHS that may contain '[...]' (same Rich-tag hazard)
|
|
584
|
+
_console.print(f" plugin dirs: {', '.join(cfg.plugin_dirs) or '(none configured)'}", markup=False)
|
|
585
|
+
_console.print(f" plugin tools ({len(tools)}): {', '.join(tools) or '(none loaded)'}", markup=False)
|
|
548
586
|
elif cmd == "/mcp":
|
|
549
587
|
configured = list(cfg.mcp_servers.keys())
|
|
550
588
|
mtools = sorted(e.name for e in base_tools.registry._tools.values()
|
|
@@ -595,8 +633,9 @@ def main() -> None:
|
|
|
595
633
|
_kw = {"base_url": _tbl.get("base_url") or "", "api_key": _tbl["api_key"]}
|
|
596
634
|
llm.switch(model=_model, reasoning=_reasoning, **_kw)
|
|
597
635
|
_stats["model"] = llm.model
|
|
636
|
+
# pin the provider ACTUALLY in effect (None DELETES a stale pin — see save_prefs)
|
|
598
637
|
save_prefs({"model": llm.model, "reasoning": llm.reasoning,
|
|
599
|
-
|
|
638
|
+
"provider": _pid or _live_pid()})
|
|
600
639
|
note = _reasoning_note(llm)
|
|
601
640
|
_console.print(f" ✓ model → [bold]{llm.model}[/]"
|
|
602
641
|
+ (f" @ [bold]{_pid}[/]" if _pid else "")
|
|
@@ -619,7 +658,9 @@ def main() -> None:
|
|
|
619
658
|
_console.print(" effort must be one of: fast | full | high | max"); return True
|
|
620
659
|
llm.switch(model=name, reasoning=eff)
|
|
621
660
|
_stats["model"] = llm.model
|
|
622
|
-
|
|
661
|
+
# typed /model keeps the endpoint — re-resolve the pin against the LIVE binding so a
|
|
662
|
+
# stale provider pin can't resurrect the old endpoint under this model at next boot
|
|
663
|
+
save_prefs({"model": llm.model, "reasoning": llm.reasoning, "provider": _live_pid()})
|
|
623
664
|
note = _reasoning_note(llm)
|
|
624
665
|
_console.print(f" ✓ model → [bold]{llm.model}[/]"
|
|
625
666
|
+ (f" · reasoning [bold]{llm.reasoning}[/]" if eff else "")
|
|
@@ -640,7 +681,7 @@ def main() -> None:
|
|
|
640
681
|
if not arg:
|
|
641
682
|
_console.print(f" workspace: {base_tools.root()}", markup=False)
|
|
642
683
|
else:
|
|
643
|
-
_console.print(" ✓ " + _reroot(arg))
|
|
684
|
+
_console.print(" ✓ " + _reroot(arg), markup=False) # path may contain '[...]' → Rich markup
|
|
644
685
|
else:
|
|
645
686
|
_console.print(f" unknown command {cmd} (/help)", markup=False)
|
|
646
687
|
return True
|
|
@@ -52,12 +52,17 @@ def load_prefs() -> dict:
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def save_prefs(updates: dict) -> None:
|
|
55
|
-
"""Merge non-empty `updates` into the prefs sidecar (atomic write)
|
|
55
|
+
"""Merge non-empty `updates` into the prefs sidecar (atomic write); an explicit None DELETES the
|
|
56
|
+
key (a stale `provider` pin must be removable — merge-only let an old endpoint pin resurrect at
|
|
57
|
+
the next boot under a model it doesn't serve). Best-effort; never raises."""
|
|
56
58
|
try:
|
|
57
59
|
import json
|
|
58
60
|
path = _prefs_path()
|
|
59
61
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
60
62
|
cur = load_prefs()
|
|
63
|
+
for k, v in updates.items():
|
|
64
|
+
if v is None:
|
|
65
|
+
cur.pop(k, None)
|
|
61
66
|
cur.update({k: v for k, v in updates.items() if v})
|
|
62
67
|
tmp = path + ".tmp"
|
|
63
68
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
@@ -208,7 +208,11 @@ class OpenAILLM:
|
|
|
208
208
|
or os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY"))
|
|
209
209
|
use_proxy = bool(proxy) and proxy != "none"
|
|
210
210
|
self.proxy_used = proxy if use_proxy else "direct" # exposed so the CLI can announce the route (A4)
|
|
211
|
-
|
|
211
|
+
# trust_env=False: the proxy is resolved EXPLICITLY above (AGENT_PROXY/HTTPS_PROXY/HTTP_PROXY →
|
|
212
|
+
# _choose_proxy), so httpx must NOT ALSO auto-read ambient proxy env — otherwise AGENT_PROXY=none
|
|
213
|
+
# ("force direct") still routes through an ambient HTTPS_PROXY (httpx defaults trust_env=True).
|
|
214
|
+
http_client = (httpx.Client(proxy=proxy, timeout=timeout, trust_env=False) if use_proxy
|
|
215
|
+
else httpx.Client(timeout=timeout, trust_env=False))
|
|
212
216
|
|
|
213
217
|
# Enforce the request timeout at the SDK layer too. The openai SDK applies its OWN per-request
|
|
214
218
|
# timeout (default ~600s) which OVERRIDES the httpx client's, so without passing it here a
|
|
@@ -278,12 +282,18 @@ class OpenAILLM:
|
|
|
278
282
|
use_proxy = bool(proxy) and proxy != "none"
|
|
279
283
|
self.proxy_used = proxy if use_proxy else "direct"
|
|
280
284
|
timeout = getattr(self, "_timeout", 60.0)
|
|
281
|
-
http_client = (httpx.Client(proxy=proxy, timeout=timeout) if use_proxy
|
|
282
|
-
else httpx.Client(timeout=timeout))
|
|
285
|
+
http_client = (httpx.Client(proxy=proxy, timeout=timeout, trust_env=False) if use_proxy
|
|
286
|
+
else httpx.Client(timeout=timeout, trust_env=False)) # explicit proxy only (see __init__)
|
|
283
287
|
ckw: dict = {"api_key": new_key}
|
|
284
288
|
if new_base:
|
|
285
289
|
ckw["base_url"] = new_base
|
|
290
|
+
old_client = getattr(self, "client", None)
|
|
286
291
|
self.client = OpenAI(http_client=http_client, timeout=timeout, max_retries=2, **ckw)
|
|
292
|
+
if old_client is not None: # close the replaced connection pool — /model hops leaked fds
|
|
293
|
+
try:
|
|
294
|
+
old_client.close()
|
|
295
|
+
except Exception: # noqa: BLE001 — cleanup must never break the switch
|
|
296
|
+
pass
|
|
287
297
|
self._base_url = new_base or ""
|
|
288
298
|
self._drop_reasoning_effort = False
|
|
289
299
|
if model:
|
|
@@ -87,6 +87,8 @@ def _is_secret(text: str) -> bool:
|
|
|
87
87
|
def _by_task(records: list[dict]) -> list[list[dict]]:
|
|
88
88
|
groups: dict[str, list[dict]] = {}
|
|
89
89
|
for r in records:
|
|
90
|
+
if not isinstance(r, dict): # a malformed episodic line (null/list/partial write, old schema)
|
|
91
|
+
continue # must not crash consolidation → silently DISCARD ALL lessons this session
|
|
90
92
|
groups.setdefault(r.get("task_id", ""), []).append(r)
|
|
91
93
|
return [sorted(g, key=lambda r: r.get("turn", 0)) for g in groups.values()]
|
|
92
94
|
|
|
@@ -98,7 +100,7 @@ def promote_episodes(records: list[dict]) -> list[dict]:
|
|
|
98
100
|
# pass 1: collect the corrective pitfall of each task + count signatures (frequency, #4)
|
|
99
101
|
cand = []
|
|
100
102
|
for recs in _by_task(records):
|
|
101
|
-
rmeta = [r.get("record", {}) for r in recs]
|
|
103
|
+
rmeta = [(r.get("record") if isinstance(r.get("record"), dict) else {}) for r in recs] # {"record": null} → {} (not None)
|
|
102
104
|
had_fail = any(m.get("meta", {}).get("failing") for m in rmeta)
|
|
103
105
|
last = rmeta[-1].get("meta", {})
|
|
104
106
|
if not (had_fail and last.get("stop_reason") == "end_turn" and not last.get("failing")):
|
|
@@ -222,7 +224,7 @@ def promote_procedures(records: list[dict], *, min_actions: int = PROC_MIN_ACTIO
|
|
|
222
224
|
EverOS rule), capped. Pure. Returns {kind, name, description, steps, files, freq, tags}."""
|
|
223
225
|
cand = []
|
|
224
226
|
for recs in _by_task(records):
|
|
225
|
-
rmeta = [r.get("record", {}) for r in recs]
|
|
227
|
+
rmeta = [(r.get("record") if isinstance(r.get("record"), dict) else {}) for r in recs] # {"record": null} → {} (not None)
|
|
226
228
|
if any(m.get("meta", {}).get("failing") for m in rmeta):
|
|
227
229
|
continue # corrective → a fact, not a procedure
|
|
228
230
|
last = rmeta[-1].get("meta", {})
|
|
@@ -140,10 +140,42 @@ def _read_config(path: str) -> dict:
|
|
|
140
140
|
try:
|
|
141
141
|
with open(path, "rb") as f:
|
|
142
142
|
return tomllib.load(f)
|
|
143
|
-
except (OSError, tomllib.TOMLDecodeError):
|
|
143
|
+
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
|
|
144
|
+
# sibling parity with config.py:_read_toml — a non-UTF-8 or malformed config must DEGRADE, not
|
|
145
|
+
# crash `sliceagent config` / `config --use` / the init wizard with a traceback.
|
|
144
146
|
return {}
|
|
145
147
|
|
|
146
148
|
|
|
149
|
+
def _backup_if_corrupt(path: str) -> "str | None":
|
|
150
|
+
"""A rewrite reads the existing config to MERGE into; but _read_config returns {} for BOTH 'missing'
|
|
151
|
+
and 'unparseable', so a rewrite over a corrupt file would silently ERASE every provider key + section
|
|
152
|
+
it couldn't parse. Guard the writers: if `path` exists and is non-empty but does NOT parse, move it
|
|
153
|
+
aside to a non-clobbering .bak first (the keys survive, recoverable) and return the backup path; else
|
|
154
|
+
None (absent / empty / valid — nothing to protect)."""
|
|
155
|
+
import tomllib
|
|
156
|
+
try:
|
|
157
|
+
with open(path, "rb") as f:
|
|
158
|
+
raw = f.read()
|
|
159
|
+
except OSError:
|
|
160
|
+
return None
|
|
161
|
+
if not raw.strip():
|
|
162
|
+
return None
|
|
163
|
+
try:
|
|
164
|
+
tomllib.loads(raw.decode("utf-8"))
|
|
165
|
+
return None # parses fine → a normal merge is safe
|
|
166
|
+
except (tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
|
|
167
|
+
pass
|
|
168
|
+
bak, n = path + ".bak", 0
|
|
169
|
+
while os.path.exists(bak): # never clobber an earlier backup
|
|
170
|
+
n += 1
|
|
171
|
+
bak = f"{path}.bak.{n}"
|
|
172
|
+
try:
|
|
173
|
+
os.replace(path, bak)
|
|
174
|
+
return bak
|
|
175
|
+
except OSError:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
147
179
|
def _atomic_write(path: str, body: str) -> None:
|
|
148
180
|
"""ATOMIC 0600 write (the file holds an API key — never leave it half-written): write a temp in the same
|
|
149
181
|
dir, fsync, then os.replace(); on ANY failure remove the temp so no key-bearing fragment is left behind."""
|
|
@@ -170,8 +202,10 @@ def _atomic_write(path: str, body: str) -> None:
|
|
|
170
202
|
pass
|
|
171
203
|
|
|
172
204
|
|
|
173
|
-
def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: str) -> None:
|
|
174
|
-
"""Merge a provider into the config: add/update [providers.<pid>], set it as the default, keep the rest.
|
|
205
|
+
def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: str) -> "str | None":
|
|
206
|
+
"""Merge a provider into the config: add/update [providers.<pid>], set it as the default, keep the rest.
|
|
207
|
+
Returns the path of a backup taken when the existing config was unparseable (else None)."""
|
|
208
|
+
bak = _backup_if_corrupt(path) # never silently erase keys in a corrupt config
|
|
175
209
|
data = _read_config(path)
|
|
176
210
|
provs = data.setdefault("providers", {})
|
|
177
211
|
if not isinstance(provs, dict): # a corrupt non-dict providers must not crash on provs[pid]=
|
|
@@ -186,6 +220,7 @@ def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: s
|
|
|
186
220
|
agent["default_provider"] = pid
|
|
187
221
|
agent["model"] = model # keep top-level model in sync (back-compat)
|
|
188
222
|
_atomic_write(path, _emit_toml(data))
|
|
223
|
+
return bak
|
|
189
224
|
|
|
190
225
|
|
|
191
226
|
def _test_key(model: str, api_key: str, base_url: str, llm_factory) -> tuple[bool, str]:
|
|
@@ -250,8 +285,20 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
|
|
|
250
285
|
for k in keys:
|
|
251
286
|
_id, label, _base, model0 = PROVIDERS[k]
|
|
252
287
|
out(f" {k}. {label}" + (f" ({model0})" if model0 else ""))
|
|
253
|
-
|
|
254
|
-
|
|
288
|
+
# an unrecognized answer must NOT silently become provider 1 (a typed "openai" used to
|
|
289
|
+
# configure OpenRouter without a word) — accept a number OR a provider id, else re-ask.
|
|
290
|
+
choice = None
|
|
291
|
+
for _attempt in range(3):
|
|
292
|
+
raw = inp(" > ").strip() or "1"
|
|
293
|
+
if raw in PROVIDERS:
|
|
294
|
+
choice = raw; break
|
|
295
|
+
byname = next((k for k in keys if PROVIDERS[k][0] == raw.lower()), None)
|
|
296
|
+
if byname:
|
|
297
|
+
choice = byname; break
|
|
298
|
+
out(f" not a valid choice: {raw!r} — enter 1-{len(PROVIDERS)} or a provider name")
|
|
299
|
+
if choice is None:
|
|
300
|
+
out(" no valid choice — aborting."); return 1
|
|
301
|
+
pid, label, base_url, model = PROVIDERS[choice]
|
|
255
302
|
if pid == "custom":
|
|
256
303
|
base_url = inp(" Base URL (OpenAI-compatible, e.g. https://host/v1): ").strip()
|
|
257
304
|
# RE-CONFIGURING an ALREADY-SAVED provider (re-running `sliceagent init` to update the model,
|
|
@@ -303,7 +350,9 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
|
|
|
303
350
|
if cont not in ("y", "yes"):
|
|
304
351
|
out(" Not saved. Re-run `sliceagent init` to try again."); return 1
|
|
305
352
|
|
|
306
|
-
_save_provider(path, pid=pid, model=model, api_key=key, base_url=base_url)
|
|
353
|
+
bak = _save_provider(path, pid=pid, model=model, api_key=key, base_url=base_url)
|
|
354
|
+
if bak:
|
|
355
|
+
out(f" ⚠ the existing config didn't parse — moved it to {bak} (its keys are recoverable there).")
|
|
307
356
|
out(f"\n Saved provider '{pid}' (model {model}) → {path} (0600)")
|
|
308
357
|
out(" Ready. Run: sliceagent\n")
|
|
309
358
|
return 0
|
|
@@ -356,7 +405,8 @@ def run_config(argv=None, *, home=None, env=None) -> int:
|
|
|
356
405
|
out(" providers (* = default · `config --use <id>` to switch):")
|
|
357
406
|
for pid, p in provs.items():
|
|
358
407
|
mark = "*" if pid == default else " "
|
|
359
|
-
|
|
408
|
+
model = p.get("model", "?") if isinstance(p, dict) else "?" # a scalar under [providers] must not crash
|
|
409
|
+
out(f" {mark} {pid} ({model})")
|
|
360
410
|
out(" set values:")
|
|
361
411
|
any_set = False
|
|
362
412
|
for e in REGISTRY:
|
|
@@ -96,11 +96,30 @@ def ask_mutations(name: str, args: dict) -> Optional[ToolDecision]:
|
|
|
96
96
|
# mode runs it without a confirm prompt. Deny-by-default: an unknown verb still asks. The catastrophic floor
|
|
97
97
|
# (no_dangerous_commands) runs BEFORE this, so e.g. `cat /etc/passwd` is still denied.
|
|
98
98
|
_RO_VERBS = frozenset((
|
|
99
|
-
"ls", "pwd", "cat", "head", "tail", "wc", "echo", "which", "type", "
|
|
99
|
+
"ls", "pwd", "cat", "head", "tail", "wc", "echo", "which", "type", "printenv", "date",
|
|
100
100
|
"whoami", "hostname", "uname", "id", "groups", "tree", "stat", "file", "du", "df", "realpath",
|
|
101
|
-
"dirname", "basename", "grep", "rg", "egrep", "fgrep", "sort", "
|
|
101
|
+
"dirname", "basename", "grep", "rg", "egrep", "fgrep", "sort", "nl", "cut", "column",
|
|
102
102
|
"cksum", "md5", "md5sum", "sha1sum", "sha256sum", "true",
|
|
103
103
|
))
|
|
104
|
+
# `env` and `uniq` were REMOVED from the allowlist: `env <program>` executes an arbitrary program, and
|
|
105
|
+
# `uniq [IN] OUT` OVERWRITES its optional second positional — neither is provably read-only, so they now
|
|
106
|
+
# take the normal confirm path. A few remaining allowlisted verbs stay read-only ONLY without a specific
|
|
107
|
+
# WRITE/EXEC option; deny-by-default when that option is present (verb-scoped so read-only siblings like
|
|
108
|
+
# `du -s` / `grep -o` keep auto-running). Value-taking flags match `-o`, `-o=…`, `-oFILE`, `--output=…`.
|
|
109
|
+
_UNSAFE_OPTS = {
|
|
110
|
+
"sort": ("-o", "--output"), # -o FILE / --output=FILE overwrite a file
|
|
111
|
+
"date": ("-s", "--set"), # set the system clock (a mutation)
|
|
112
|
+
"tree": ("-o",), # -o FILE writes the listing to a file
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _has_unsafe_opt(verb: str, toks: list) -> bool:
|
|
117
|
+
bad = _UNSAFE_OPTS.get(verb, ())
|
|
118
|
+
for t in toks[1:]:
|
|
119
|
+
for b in bad:
|
|
120
|
+
if t == b or t.startswith(b + "=") or (len(b) == 2 and len(t) > 2 and t.startswith(b)):
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
104
123
|
# git subcommands with NO mutating form (excludes branch/tag/config/remote/stash/reflog, which can write).
|
|
105
124
|
_RO_GIT_SUB = frozenset((
|
|
106
125
|
"status", "log", "diff", "show", "rev-parse", "ls-files", "ls-tree", "describe", "blame",
|
|
@@ -127,10 +146,15 @@ def _is_readonly_command(cmd: str) -> bool:
|
|
|
127
146
|
verb = os.path.basename(toks[0])
|
|
128
147
|
if verb == "git":
|
|
129
148
|
sub = next((t for t in toks[1:] if not t.startswith("-")), "")
|
|
130
|
-
|
|
149
|
+
if sub not in _RO_GIT_SUB:
|
|
150
|
+
return False
|
|
151
|
+
# `git grep --open-files-in-pager[=<cmd>]` / `-O[<cmd>]` runs a PAGER program (arbitrary exec).
|
|
152
|
+
if sub == "grep" and any(t.startswith("--open-files-in-pager") or t.startswith("-O") for t in toks):
|
|
153
|
+
return False
|
|
154
|
+
return True
|
|
131
155
|
if verb == "find":
|
|
132
156
|
return not any(t in _FIND_MUTATORS for t in toks)
|
|
133
|
-
return verb in _RO_VERBS
|
|
157
|
+
return verb in _RO_VERBS and not _has_unsafe_opt(verb, toks)
|
|
134
158
|
|
|
135
159
|
|
|
136
160
|
def ask_commands(name: str, args: dict) -> Optional[ToolDecision]:
|
|
@@ -747,14 +747,20 @@ class LocalToolHost:
|
|
|
747
747
|
|
|
748
748
|
@staticmethod
|
|
749
749
|
def _detect_crlf(full: str) -> bool:
|
|
750
|
-
"""True if the existing file
|
|
750
|
+
"""True if the existing file is DOMINANTLY Windows CRLF (sample the head). Used to PRESERVE
|
|
751
751
|
line endings on edit: the model emits '\\n', and writing that to a CRLF file rewrites every line
|
|
752
|
-
ending — a huge spurious diff / corruption on Windows-authored repos.
|
|
752
|
+
ending — a huge spurious diff / corruption on Windows-authored repos. DOMINANCE (not mere
|
|
753
|
+
presence): a mostly-LF file with one embedded '\\r\\n' (a byte literal, an HTTP fixture, a merge
|
|
754
|
+
artifact) must NOT be flipped whole-file to CRLF — while a uniformly-CRLF file with one stray LF
|
|
755
|
+
still counts as CRLF. crlf ≥ (bare-LF) covers both, and keeps the pinned uniform cases."""
|
|
753
756
|
try:
|
|
754
757
|
with open(full, "rb") as f:
|
|
755
|
-
|
|
758
|
+
head = f.read(65536)
|
|
756
759
|
except OSError:
|
|
757
760
|
return False
|
|
761
|
+
crlf = head.count(b"\r\n")
|
|
762
|
+
lf_only = head.count(b"\n") - crlf # LFs that are NOT part of a CRLF
|
|
763
|
+
return crlf > 0 and crlf >= lf_only
|
|
758
764
|
|
|
759
765
|
@staticmethod
|
|
760
766
|
def _preserve_eol(text: str, crlf: bool) -> str:
|
|
@@ -864,12 +864,16 @@ def _model_candidates(llm, cfg):
|
|
|
864
864
|
|
|
865
865
|
def _reasoning_levels(model, base_url):
|
|
866
866
|
"""Reasoning levels valid for a model, derived from its capability (provider-aware). Effort-capable
|
|
867
|
-
models (gpt-5/o-series) expose all four;
|
|
867
|
+
models (gpt-5/o-series) expose all four; OpenRouter exposes fast/full/high (its unified reasoning
|
|
868
|
+
object maps max→high, so offering max would be a lie); others only fast/full (high/max would
|
|
869
|
+
degrade to default)."""
|
|
868
870
|
from .model_catalog import capability
|
|
869
871
|
full4 = [("fast", "minimal reasoning — fastest, cheapest"),
|
|
870
872
|
("full", "provider default reasoning"),
|
|
871
873
|
("high", "deeper reasoning (effort=high, /v1/responses)"),
|
|
872
874
|
("max", "deepest reasoning (effort=xhigh)")]
|
|
875
|
+
if "openrouter" in (base_url or "").lower(): # unified reasoning object honors effort WITH tools
|
|
876
|
+
return full4[:2] + [("high", "deeper reasoning (unified reasoning, effort=high)")]
|
|
873
877
|
if capability(model, base_url).supports_reasoning_effort:
|
|
874
878
|
return full4
|
|
875
879
|
return full4[:2] # fast | full only — the model has no effort knob
|
|
@@ -888,7 +892,10 @@ def select_model_reasoning(llm, cfg, *, pt_input=None, pt_output=None):
|
|
|
888
892
|
if pick is None:
|
|
889
893
|
return None
|
|
890
894
|
model, _grp, pid = cands[pick]
|
|
891
|
-
|
|
895
|
+
if pid: # the pick will REBIND to this provider — offer the levels ITS endpoint supports
|
|
896
|
+
base = ((cfg.providers() or {}).get(pid) or {}).get("base_url") or ""
|
|
897
|
+
else:
|
|
898
|
+
base = getattr(llm, "_base_url", "") if model == llm.model else ""
|
|
892
899
|
levels = _reasoning_levels(model, base)
|
|
893
900
|
lvl_rows = [(name, desc) for name, desc in levels]
|
|
894
901
|
lvl_cur = next((i for i, (n, _) in enumerate(levels) if n == llm.reasoning), -1)
|
|
@@ -918,7 +918,10 @@ def cost_chart_renders_flat_vs_rising():
|
|
|
918
918
|
import sys
|
|
919
919
|
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
920
920
|
sys.path.insert(0, os.path.join(root, "evals"))
|
|
921
|
-
|
|
921
|
+
try:
|
|
922
|
+
import realenv_multiturn as rem # importable WITHOUT the heavy swebench dep
|
|
923
|
+
except ModuleNotFoundError:
|
|
924
|
+
return # evals/ is a local-only dev tree, not shipped in the installed package (CI) — skip, don't fail
|
|
922
925
|
rows = [{"turn": i, "peak_in": 6000, "transcript": 6000 * i} for i in range(1, 13)]
|
|
923
926
|
chart = rem.render_cost_chart(rows)
|
|
924
927
|
assert "sliceagent" in chart and "transcript" in chart and "12×" in chart, chart
|
|
@@ -1344,6 +1347,80 @@ def model_switch_warns_when_the_endpoint_cant_serve_it():
|
|
|
1344
1347
|
assert "OpenAI" not in _reasoning_note(LLM("gpt-5.5", "", "high"))
|
|
1345
1348
|
|
|
1346
1349
|
|
|
1350
|
+
# ── Round 2 (2026-07-03) — deep-core hunt confirmed fixes ─────────────────────────────────────
|
|
1351
|
+
@check
|
|
1352
|
+
def durable_log_redacts_secrets_in_NESTED_tool_args():
|
|
1353
|
+
# HIGH: _scrub_args redacted only top-level string values → a secret in a nested dict/list arg
|
|
1354
|
+
# (an MCP tool's {config:{api_key:…}} / {headers:{Authorization:…}}) hit the on-disk log in plaintext.
|
|
1355
|
+
from sliceagent.cli import log_sink
|
|
1356
|
+
from sliceagent.events import ToolResult
|
|
1357
|
+
d = tempfile.mkdtemp(); path = os.path.join(d, "log.jsonl")
|
|
1358
|
+
sink = log_sink(root=d, path=path)
|
|
1359
|
+
secret = "sk-verylongsecretkey1234567890abcdefghij"
|
|
1360
|
+
tok = "ghp_1234567890abcdefghijklmnopqrstuvwx"
|
|
1361
|
+
sink(ToolResult("mcp_call", {"config": {"api_key": secret}, "items": [tok]}, "ok", False))
|
|
1362
|
+
body = open(path).read()
|
|
1363
|
+
assert secret not in body, "a secret in a NESTED dict arg must be redacted before the durable log"
|
|
1364
|
+
assert tok not in body, "a secret in a NESTED list arg must be redacted too"
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
@check
|
|
1368
|
+
def detect_crlf_is_dominance_not_mere_presence():
|
|
1369
|
+
from sliceagent.tools import LocalToolHost
|
|
1370
|
+
h = LocalToolHost(tempfile.mkdtemp())
|
|
1371
|
+
def probe(name, data):
|
|
1372
|
+
p = os.path.join(h.root(), name); open(p, "wb").write(data); return h._detect_crlf(p)
|
|
1373
|
+
assert probe("u_crlf", b"a\r\nb\r\nc\r\n") is True # uniform CRLF → CRLF (pinned)
|
|
1374
|
+
assert probe("u_lf", b"a\nb\nc\n") is False # uniform LF → LF (pinned)
|
|
1375
|
+
assert probe("mixed", b'x = b"h\r\n"\np = 8080\nd = 1\n') is False # mostly-LF + 1 CRLF → LF (the fix)
|
|
1376
|
+
assert probe("winstray", b"a\r\nb\r\nc\r\nd\n") is True # CRLF-dominant + 1 stray LF → CRLF
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
@check
|
|
1380
|
+
def consolidation_survives_a_malformed_episodic_record():
|
|
1381
|
+
from sliceagent.neocortex import promote_episodes, promote_procedures
|
|
1382
|
+
good = [{"task_id": "t", "turn": 1, "record": {"meta": {"failing": True}}},
|
|
1383
|
+
{"task_id": "t", "turn": 2, "record": {"meta": {"failing": False, "stop_reason": "end_turn"}}}]
|
|
1384
|
+
for bad in (None, [1, 2], "str", 42, {"record": None}): # each a malformed line among good ones
|
|
1385
|
+
promote_episodes(good + [bad]) # must not raise (was AttributeError → lost ALL lessons)
|
|
1386
|
+
promote_procedures(good + [bad])
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
@check
|
|
1390
|
+
def direct_llm_client_ignores_ambient_proxy_env():
|
|
1391
|
+
import sliceagent.llm as _llm
|
|
1392
|
+
saved = {k: os.environ.get(k) for k in ("AGENT_PROXY", "HTTPS_PROXY", "HTTP_PROXY", "LLM_API_KEY")}
|
|
1393
|
+
try:
|
|
1394
|
+
os.environ.update({"AGENT_PROXY": "none", "HTTPS_PROXY": "http://ambient:7890", "LLM_API_KEY": "k"})
|
|
1395
|
+
llm = _llm.OpenAILLM(model="deepseek-chat", base_url="https://api.deepseek.com/v1")
|
|
1396
|
+
assert llm.proxy_used == "direct"
|
|
1397
|
+
assert llm.client._client.trust_env is False, "AGENT_PROXY=none must truly force direct (no ambient proxy)"
|
|
1398
|
+
llm.switch(model="gpt-5.5", base_url="", api_key="k2")
|
|
1399
|
+
assert llm.client._client.trust_env is False, "the switch() direct client must ignore ambient proxy too"
|
|
1400
|
+
finally:
|
|
1401
|
+
for k, v in saved.items():
|
|
1402
|
+
os.environ.pop(k, None) if v is None else os.environ.__setitem__(k, v)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
@check
|
|
1406
|
+
def slash_handlers_print_bracketed_paths_without_markup_crash():
|
|
1407
|
+
# /undo and /cwd echo a workspace PATH; a Next.js '[id]' segment is a Rich tag → MarkupError.
|
|
1408
|
+
from io import StringIO
|
|
1409
|
+
|
|
1410
|
+
from rich.console import Console
|
|
1411
|
+
con = Console(file=StringIO(), force_terminal=False)
|
|
1412
|
+
# a real /cwd echo: `_reroot("~/proj/[/]")` returns "not a directory: ~/proj/[/]" — '[/]' is a Rich
|
|
1413
|
+
# closing tag with nothing to close (the verifiers' exact repro).
|
|
1414
|
+
con.print(" ✓ not a directory: ~/proj/[/]", markup=False) # must not raise
|
|
1415
|
+
con.print(" Undid the last edit to app/[id]/page.tsx (1 change).", markup=False)
|
|
1416
|
+
raised = False
|
|
1417
|
+
try:
|
|
1418
|
+
con.print("not a directory: ~/proj/[/]") # markup ON → the MarkupError the fix avoids
|
|
1419
|
+
except Exception: # noqa: BLE001
|
|
1420
|
+
raised = True
|
|
1421
|
+
assert raised, "sanity: '[/]' DOES break Rich with markup on (so markup=False is load-bearing)"
|
|
1422
|
+
|
|
1423
|
+
|
|
1347
1424
|
def main():
|
|
1348
1425
|
failed = 0
|
|
1349
1426
|
for fn in CHECKS:
|