tether-sdk 0.2.5__tar.gz → 0.2.6__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.
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/AGENTS.md +2 -2
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/PKG-INFO +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/README.md +2 -2
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/grade.py +42 -1
- tether_sdk-0.2.6/bench/harness-bench/README.md +25 -0
- tether_sdk-0.2.6/bench/harness-bench/tether-adapter.patch +134 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/run.py +1 -0
- tether_sdk-0.2.6/bench/tasks/codereview/goal.txt +1 -0
- tether_sdk-0.2.6/bench/tasks/codereview/ws/inventory.py +40 -0
- tether_sdk-0.2.6/bench/tasks/plan/goal.txt +1 -0
- tether_sdk-0.2.6/bench/tasks/plan/ws/README.md +3 -0
- tether_sdk-0.2.6/bench/tasks/plan/ws/svc/client.py +18 -0
- tether_sdk-0.2.6/bench/tasks/plan/ws/svc/config.py +3 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/release-notes.md +13 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/testing.md +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/security-and-sandbox.md +1 -1
- tether_sdk-0.2.6/e2e/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_limits.py +17 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_sandbox.py +15 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_cli.py +16 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_harness.py +57 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_security_hardening.py +52 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_store.py +23 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_tether.py +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/__init__.py +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/cli.py +15 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/harness.py +22 -2
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/sandbox.py +30 -9
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/store.py +18 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/.claude/feature-context/active/.gitkeep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/.claude/feature-context/active/v0-2-bugs.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/.claude/feature-context/archive/.gitkeep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/.github/workflows/ci.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/.github/workflows/delivery.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/.github/workflows/docs.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/.gitignore +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/LICENSE +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/README.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/naive_agent.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ref/calc/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ref/calc/errors.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ref/calc/evaluator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ref/calc/parser.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ref/calc/tokenizer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ws/calc/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ws/calc/errors.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ws/calc/evaluator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ws/calc/parser.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ws/calc/tokenizer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-hard/ws/tests/test_calc.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/ws/cli.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/ws/ledger/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/ws/ledger/parser.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/ws/ledger/report.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/ws/sample.csv +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-medium/ws/tests/test_report.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-simple/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-simple/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-simple/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-simple/ws/tests/test_textutils.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/code-simple/ws/textutils.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/content/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/content/ws/.keep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/AGENTS.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/README.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/docs/architecture.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/docs/style.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/mini/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/mini/cli.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/mini/fmt.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/mini/stats.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/mini/textio.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/focus/ws/tests/test_stats.py +0 -0
- {tether_sdk-0.2.5/e2e → tether_sdk-0.2.6/bench/tasks/plan/ws/svc}/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/qa/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/qa/ws/.keep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/review/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/bench/tasks/review/ws/paper.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/architecture.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/benchmarks.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/extending.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/index.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/javascripts/mathjax.js +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/agent-interface.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/context-and-store.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/harness.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/index.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/multiplexer-and-decorator.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/providers.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/sandbox-and-skills.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/reference/tools.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/releasing.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/troubleshooting.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/agent-interface.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/built-in-tools.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/cli-and-headless.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/index.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/multi-agent.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/sdk-and-custom-tools.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/skills.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/docs/tutorial/workflows-and-telemetry.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/conftest.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/proxy.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_agents.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_defaults.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_faults.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_lifecycle.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_quirks.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/e2e/test_surface.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/README.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/checks.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/course.yaml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/ruff.toml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/skills/planner/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/skills/searcher/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/skills/searcher/tools.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/skills/writer/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/examples/course/skills/writer/tools.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/mkdocs.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/pyproject.toml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/scripts/record_golden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/skills/researcher/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/conftest.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/fixtures/dialects.jsonl +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/coding_edit_failure_modes.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/coding_edit_failure_modes.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/done_nudge.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/done_nudge.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/read_range_pingpong.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/read_range_pingpong.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/research_web.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/research_web.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/shell_wrappers.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/shell_wrappers.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/waiting_input_resume.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/golden/waiting_input_resume.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/test_anthropic.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/test_base.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/test_declarative.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/test_dialects.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/test_ollama.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/test_openai.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/providers/test_retry.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_agent_interface.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_context.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_context_config.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_decorator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_edit_tool.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_golden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_integration.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_misses.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_multiplexer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_params.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_pipeline.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_release_hardening.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_retro.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_sandbox.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_skills.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/test_structured_history.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/tools/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/tools/test_agent.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/tools/test_bash.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/tools/test_done.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/tools/test_edit.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/tools/test_mem.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tests/tools/test_web.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/api.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/context.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/decorator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/mcp.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/multiplexer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/pipeline.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/providers/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/providers/anthropic.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/providers/base.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/providers/declarative.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/providers/ollama.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/providers/openai.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/retro.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/schema.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/skills.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/tools/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/tools/agent.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/tools/bash.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/tools/done.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/tools/edit.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/tools/mem.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/tether/tools/web.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.6}/uv.lock +0 -0
|
@@ -45,7 +45,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
|
|
|
45
45
|
- Sessions do **not** terminate simply because the model emitted a text response without a tool call (small 9B models frequently "think out loud" before acting).
|
|
46
46
|
- A task only completes when the agent explicitly calls `act("done", stdin="<final deliverable>")` (or `act("done '<short answer>'")`), pauses for human/parent input via `act("done --ask", stdin="<question>")` (transitioning the session to `WAITING_INPUT` and releasing the worker process until resumed via `send`), or hits `max_steps`.
|
|
47
47
|
- If the model emits plain text without calling `act`, the harness appends a brief deterministic nudge reminding it to either execute a command or call `done`.
|
|
48
|
-
- After 6 consecutive read-only/verification steps following a change, the harness appends a one-shot `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete (0 static prompt tokens, #74). Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change end the session `FAILED` as `Stalled` (#92); `tether stop <id>` terminates a detached worker (#89).
|
|
48
|
+
- After 6 consecutive read-only/verification steps following a change, the harness appends a one-shot `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete (0 static prompt tokens, #74). Before any change, 12 read-only steps in a row (then every 8 more) append a one-line `[NUDGE]` to make the edit or call `done` (#127). Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change end the session `FAILED` as `Stalled` (#92); `tether stop <id>` terminates a detached worker (#89).
|
|
49
49
|
- When `verify` gate commands are configured (via `Harness(verify=[...])`, CLI `--verify`, or Skill YAML `verify: [...]`), `Harness.execute_session` deterministically executes each command inside the session's `SandboxPolicy` before allowing `done` to transition to `COMPLETED` (consuming 0 static prompt tokens until a gate fails). A gate that errors instead of failing (sandbox violation, exit 124/126/127) ends the session `FAILED` at once as `gate_error` ("gate misconfigured"), without counting toward the consecutive-failure abort.
|
|
50
50
|
|
|
51
51
|
### Rule 4: Progressive `--help` (JIT Schema Disclosure)
|
|
@@ -71,7 +71,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
|
|
|
71
71
|
- All `edit` and `bash` paths are canonicalized (`Path.resolve()`) and confined to `workspace_root`.
|
|
72
72
|
- Sensitive files/directories (`.env*`, `.git/config`, `.git/hooks`, `~/.ssh`, `.tether/`) are deterministically protected from mutation or read exfiltration.
|
|
73
73
|
- `bash` subprocesses get an allowlisted environment only (secrets with any name are dropped; opt-in via `TETHER_ENV_PASSTHROUGH`). `web fetch` blocks loopback/private/metadata addresses (SSRF guard). `SandboxPolicy(network=False)` / `--no-network` adds `--unshare-net` (fails closed without `bwrap`; children can only narrow it).
|
|
74
|
-
- On Linux, `bash` automatically uses **`bwrap` (Bubblewrap) / `unshare`** when available (read-only `/`, hidden `/home` and `/var/tmp` tmpfs (existing `PATH` dirs under them, e.g. `~/.local/bin`, are re-bound read-only so `uv`/`node` still run, #76), `/run` unix sockets such as `docker.sock` masked, read-write bind on `workspace_root` with secret-looking files at any depth (`.env*`, `*.pem`, `*.key`, ...), `.tether/`, `.git/config` and `.git/hooks` masked at the mount level, `PR_SET_NO_NEW_PRIVS`), falling back to a strict deterministic command allowlist/denylist (`rm -rf /`, `sudo`, `dd`, `mkfs`, `chmod 777`, etc. blocked).
|
|
74
|
+
- On Linux, `bash` automatically uses **`bwrap` (Bubblewrap) / `unshare`** when available (read-only `/`, hidden `/home` and `/var/tmp` tmpfs (existing `PATH` dirs under them, e.g. `~/.local/bin`, are re-bound read-only so `uv`/`node` still run, #76), `/run` unix sockets such as `docker.sock` masked, read-write bind on `workspace_root` with secret-looking files at any depth (`.env*`, `*.pem`, `*.key`, ...), `.tether/`, `.git/config` and `.git/hooks` masked at the mount level, `PR_SET_NO_NEW_PRIVS`), falling back to a strict deterministic command allowlist/denylist (path, `..` and `.env` text rules run only in this fallback, the mounts enforce them under `bwrap`, #125; `.env.example`-style templates are not secrets) (`rm -rf /`, `sudo`, `dd`, `mkfs`, `chmod 777`, etc. blocked).
|
|
75
75
|
- Sub-agents spawned via `agent spawn` strictly inherit or narrow their parent session's `SandboxPolicy` (no privilege escalation).
|
|
76
76
|
|
|
77
77
|
### Rule 7: Zero Framework Bloat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tether-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: General-purpose, headless-first LLM-Native micro-harness for small language models
|
|
5
5
|
Project-URL: Homepage, https://github.com/igormcsouza/tether
|
|
6
6
|
Project-URL: Repository, https://github.com/igormcsouza/tether
|
|
@@ -4,8 +4,8 @@ Not part of the `tether` package, not run in CI. Use it to onboard a new model o
|
|
|
4
4
|
release for regressions that deterministic tests cannot see (unit, golden and e2e tests stay the
|
|
5
5
|
gate; see `docs/testing.md`).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
`focus` (one exact edit in a distractor-heavy repo, scope-checked), `code-simple`, `code-medium`, `code-hard` (graded by hidden unit tests; editing the visible tests
|
|
7
|
+
Nine tasks in `tasks/<name>/` (workspace in `ws/`, goal in `goal.txt`, hidden tests in `hidden/`):
|
|
8
|
+
`focus` (one exact edit in a distractor-heavy repo, scope-checked), `plan` (an implementation plan, checked for named files, failure handling and tests), `codereview` (eight seeded bugs to find, no edits), `code-simple`, `code-medium`, `code-hard` (graded by hidden unit tests; editing the visible tests
|
|
9
9
|
scores 0), `qa`, `content`, `review` (graded by constraint counts and keyword checks, so they are
|
|
10
10
|
a smoke test for quality, not a judge).
|
|
11
11
|
|
|
@@ -135,8 +135,49 @@ def _focus(task, ws, result):
|
|
|
135
135
|
return r
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def _steps(t):
|
|
139
|
+
return len(re.findall(r"^\s*\d+[.)]", t, re.M))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _plan(_task, _ws, result):
|
|
143
|
+
t = result or ""
|
|
144
|
+
c = {
|
|
145
|
+
"6-10 numbered steps": 6 <= _steps(t) <= 10,
|
|
146
|
+
"names client.py": "client.py" in t,
|
|
147
|
+
"names config.py": "config.py" in t,
|
|
148
|
+
"names get/post": bool(re.search(r"\bget\b", t)) and bool(re.search(r"\bpost\b", t)),
|
|
149
|
+
"backoff+jitter": bool(re.search(r"exponential", t, re.I)) and bool(re.search(r"jitter", t, re.I)),
|
|
150
|
+
"retry settings": bool(re.search(r"max[_ -]?(retries|attempts)|retry[_ -]?(limit|count)", t, re.I)) and bool(re.search(r"base[_ -]?(delay|backoff)|backoff[_ -]?(base|factor)", t, re.I)),
|
|
151
|
+
"429 and 5xx": "429" in t and bool(re.search(r"5xx|50\d", t)),
|
|
152
|
+
"not retried (4xx)": bool(re.search(r"4xx|not retr|never retr|do not retry|don't retry|non-retry", t, re.I)),
|
|
153
|
+
"Retry-After": bool(re.search(r"retry-after", t, re.I)),
|
|
154
|
+
"tests without network": bool(re.search(r"mock|fake|stub|patch|monkeypatch|inject", t, re.I)) and bool(re.search(r"test", t, re.I)),
|
|
155
|
+
"idempotency of post": bool(re.search(r"idempot|post.{0,60}(retry|retri)|(retry|retri).{0,60}post", t, re.I)),
|
|
156
|
+
}
|
|
157
|
+
return {"score": round(sum(c.values()) / len(c), 3), "pass": sum(c.values()) >= 9, "detail": {"missed": [k for k, v in c.items() if not v]}}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
BUGS = {
|
|
161
|
+
"page off-by-one": r"page.{0,200}(off.by.one|number\s*-\s*1|\(number - 1\)|1-based|zero|skips? the first)|(off.by.one|1-based).{0,200}page",
|
|
162
|
+
"mutable default": r"mutable default|tags=\[\]|default (argument|arg|list)|shared (between|across)",
|
|
163
|
+
"float money": r"float.{0,120}(money|price|currenc|rounding|precision|decimal)|decimal",
|
|
164
|
+
"float equality": r"(==|equal).{0,100}(float|0\.0)|is_free|float.{0,60}(compar|equal)|isclose",
|
|
165
|
+
"sql injection": r"sql.?inject|parameteri[sz]|placeholder|string concat|bind (var|param)",
|
|
166
|
+
"file not closed": r"(not|never) clos|resource leak|context manager|with open|leak",
|
|
167
|
+
"keyerror/missing": r"keyerror|missing (key|item|name)|\.get\(|unknown name|not in (the )?(stock|dict)",
|
|
168
|
+
"split malformed": r"split.{0,100}(more than|exactly|two|malformed|blank|empty|valueerror)|blank line|malformed|empty line|valueerror",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _codereview(_task, _ws, result):
|
|
173
|
+
t = result or ""
|
|
174
|
+
hit = {k: bool(re.search(v, t, re.I | re.S)) for k, v in BUGS.items()}
|
|
175
|
+
must = ["page off-by-one", "mutable default", "sql injection", "file not closed", "keyerror/missing"]
|
|
176
|
+
return {"score": round(sum(hit.values()) / len(hit), 3), "pass": all(hit[k] for k in must), "detail": {"missed": [k for k, v in hit.items() if not v], "items": _steps(t)}}
|
|
177
|
+
|
|
178
|
+
|
|
138
179
|
def grade(task, ws, result):
|
|
139
|
-
fn = {"qa": _qa, "content": _content, "review": _review, "focus": _focus}.get(task, _code)
|
|
180
|
+
fn = {"qa": _qa, "content": _content, "review": _review, "focus": _focus, "plan": _plan, "codereview": _codereview}.get(task, _code)
|
|
140
181
|
try:
|
|
141
182
|
return fn(task, ws, result)
|
|
142
183
|
except Exception as e: # grader bug must never look like a model result
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Harness-Bench adapter (pilot, not yet run end to end)
|
|
2
|
+
|
|
3
|
+
[Harness-Bench](https://github.com/Qihoo360/harness-bench) ([paper](https://arxiv.org/abs/2605.27922)) scores
|
|
4
|
+
whole harnesses on 274 offline workflow tasks (plus 26 multimodal) with hidden oracles. This directory holds
|
|
5
|
+
the patch that registers Tether as an eighth harness. It is a way to benchmark, not a Tether feature.
|
|
6
|
+
|
|
7
|
+
`tether-adapter.patch` applies to harness-bench commit `b8d89634c6f445fcc68ceb6744a59bef6cdf4863`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/Qihoo360/harness-bench && cd harness-bench
|
|
11
|
+
git apply /path/to/tether/bench/harness-bench/tether-adapter.patch
|
|
12
|
+
uv sync --extra dev
|
|
13
|
+
export PYTHONPATH=$PWD/src:$PWD/tools:$PWD/tools/task_maintenance
|
|
14
|
+
python tools/run_benchmark.py --base-url "$URL" --model "$MODEL" --harness tether \
|
|
15
|
+
--tasks 001-meeting-summary --workers 1 --run-root /tmp/hb-run
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
What the patch adds: `docker/harnesses/tether/` (their toolchain image plus `tether-sdk`, an entrypoint that runs
|
|
19
|
+
`tether run --tools bash,edit` with state kept outside the graded workspace), a `tether` `HarnessSpec`, and a
|
|
20
|
+
`providers.yaml` renderer.
|
|
21
|
+
|
|
22
|
+
Status (2026-10-23): image builds, `tether run` starts inside the trial container and reaches `ProviderError ...
|
|
23
|
+
ConnectError`, because on the machine used containers cannot reach host ports (`host.docker.internal`, where the
|
|
24
|
+
per-trial model proxy listens). The host firewall must allow the docker bridge to the proxy's ports. No task has
|
|
25
|
+
been scored yet.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
diff --git a/docker/harnesses/tether/Dockerfile b/docker/harnesses/tether/Dockerfile
|
|
2
|
+
new file mode 100644
|
|
3
|
+
index 0000000..d9f3e28
|
|
4
|
+
--- /dev/null
|
|
5
|
+
+++ b/docker/harnesses/tether/Dockerfile
|
|
6
|
+
@@ -0,0 +1,45 @@
|
|
7
|
+
+FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b AS task-toolchain
|
|
8
|
+
+
|
|
9
|
+
+ARG PIP_INDEX_URL
|
|
10
|
+
+ARG PIP_EXTRA_INDEX_URL
|
|
11
|
+
+ARG PIP_TRUSTED_HOST
|
|
12
|
+
+COPY docker/openclaw/requirements.lock /tmp/harnessbench-requirements.lock
|
|
13
|
+
+RUN python -m pip install --disable-pip-version-check --no-cache-dir \
|
|
14
|
+
+ --requirement /tmp/harnessbench-requirements.lock \
|
|
15
|
+
+ && rm /tmp/harnessbench-requirements.lock
|
|
16
|
+
+
|
|
17
|
+
+FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
|
|
18
|
+
+
|
|
19
|
+
+ARG TETHER_VERSION=0.2.5
|
|
20
|
+
+ARG PYPI_INDEX_URL=https://pypi.org/simple
|
|
21
|
+
+RUN apt-get update \
|
|
22
|
+
+ && apt-get install -y --no-install-recommends bash git ripgrep sqlite3 \
|
|
23
|
+
+ && rm -rf /var/lib/apt/lists/*
|
|
24
|
+
+
|
|
25
|
+
+
|
|
26
|
+
+
|
|
27
|
+
+RUN useradd --create-home --uid 1000 harness
|
|
28
|
+
+COPY --from=task-toolchain /usr/local/ /usr/local/
|
|
29
|
+
+COPY docker/openclaw/verify_toolchain.py /opt/harnessbench/verify_toolchain.py
|
|
30
|
+
+COPY docker/harnesses/tether/entrypoint.sh /opt/harness/bin/run
|
|
31
|
+
+RUN ln -sf /usr/local/bin/python3 /usr/bin/python3 \
|
|
32
|
+
+ && python /opt/harnessbench/verify_toolchain.py --python-only \
|
|
33
|
+
+ && python -m venv /opt/tether \
|
|
34
|
+
+ && /opt/tether/bin/python -m pip install --no-cache-dir \
|
|
35
|
+
+ --index-url "${PYPI_INDEX_URL}" "tether-sdk==${TETHER_VERSION}" \
|
|
36
|
+
+ && chmod 0755 /opt/harness/bin/run
|
|
37
|
+
+
|
|
38
|
+
+ENV PATH="/opt/harnessbench/frontend-bin:/usr/local/bin:/opt/tether/bin:${PATH}" \
|
|
39
|
+
+ NANOBOT_STREAM_IDLE_TIMEOUT_S=7200 \
|
|
40
|
+
+ NANOBOT_OPENAI_COMPAT_TIMEOUT_S=7200 \
|
|
41
|
+
+ NANOBOT_LLM_TIMEOUT_S=0
|
|
42
|
+
+
|
|
43
|
+
+# BEGIN NANOBOT LOGIN-SHELL PATH
|
|
44
|
+
+RUN printf '%s\n' \
|
|
45
|
+
+ 'export PATH="/usr/local/bin:/opt/tether/bin:$PATH"' \
|
|
46
|
+
+ > /etc/profile.d/harnessbench-path.sh \
|
|
47
|
+
+ && chmod 0644 /etc/profile.d/harnessbench-path.sh
|
|
48
|
+
+# END NANOBOT LOGIN-SHELL PATH
|
|
49
|
+
+
|
|
50
|
+
+USER 1000:1000
|
|
51
|
+
+ENTRYPOINT ["/opt/harness/bin/run"]
|
|
52
|
+
diff --git a/docker/harnesses/tether/entrypoint.sh b/docker/harnesses/tether/entrypoint.sh
|
|
53
|
+
new file mode 100644
|
|
54
|
+
index 0000000..105cee6
|
|
55
|
+
--- /dev/null
|
|
56
|
+
+++ b/docker/harnesses/tether/entrypoint.sh
|
|
57
|
+
@@ -0,0 +1,38 @@
|
|
58
|
+
+#!/usr/bin/env bash
|
|
59
|
+
+set -euo pipefail
|
|
60
|
+
+
|
|
61
|
+
+: "${HARNESSBENCH_PROMPT:?}"
|
|
62
|
+
+: "${HARNESSBENCH_CONFIG:?}"
|
|
63
|
+
+: "${HARNESSBENCH_WORKSPACE:?}"
|
|
64
|
+
+: "${HARNESSBENCH_STATE:?}"
|
|
65
|
+
+: "${HARNESSBENCH_TRACE:?}"
|
|
66
|
+
+
|
|
67
|
+
+export HOME="${HARNESSBENCH_STATE}/home"
|
|
68
|
+
+mkdir -p "${HOME}" "${HARNESSBENCH_TRACE}"
|
|
69
|
+
+runtime_config="${HARNESSBENCH_STATE}/providers.yaml"
|
|
70
|
+
+cp "${HARNESSBENCH_CONFIG}" "${runtime_config}"
|
|
71
|
+
+# The render step writes `# model: <id>` and `# turns: <n>` on the first lines.
|
|
72
|
+
+model="$(sed -n 's/^# model: //p' "${runtime_config}" | head -1)"
|
|
73
|
+
+turns="$(sed -n 's/^# turns: //p' "${runtime_config}" | head -1)"
|
|
74
|
+
+prompt="$(<"${HARNESSBENCH_PROMPT}")"
|
|
75
|
+
+
|
|
76
|
+
+export TETHER_PROVIDERS="${runtime_config}"
|
|
77
|
+
+export VLLM_API_KEY="${VLLM_API_KEY:-local-vllm-no-auth}" # placeholder; the trial proxy injects the real key
|
|
78
|
+
+export TETHER_DB="${HARNESSBENCH_STATE}/tether.db" # state stays outside the graded workspace
|
|
79
|
+
+export TETHER_ENV_PASSTHROUGH="PATH,NODE_PATH,GIT_CONFIG_GLOBAL"
|
|
80
|
+
+
|
|
81
|
+
+set +e
|
|
82
|
+
+cd "${HARNESSBENCH_WORKSPACE}"
|
|
83
|
+
+tether run \
|
|
84
|
+
+ --model "harnessbench/${model}" \
|
|
85
|
+
+ --tools bash,edit \
|
|
86
|
+
+ --max-steps "${turns:-150}" \
|
|
87
|
+
+ "${prompt}" \
|
|
88
|
+
+ > >(tee "${HARNESSBENCH_TRACE}/events.log") \
|
|
89
|
+
+ 2> >(tee "${HARNESSBENCH_TRACE}/stderr.log" >&2)
|
|
90
|
+
+status=$?
|
|
91
|
+
+set -e
|
|
92
|
+
+
|
|
93
|
+
+[[ -f "${TETHER_DB}" ]] && cp "${TETHER_DB}" "${HARNESSBENCH_TRACE}/tether.db"
|
|
94
|
+
+chmod -R a+rX "${HARNESSBENCH_WORKSPACE}" "${HARNESSBENCH_TRACE}" 2>/dev/null || true
|
|
95
|
+
+exit "${status}"
|
|
96
|
+
diff --git a/tools/harness_runtime.py b/tools/harness_runtime.py
|
|
97
|
+
index c57ba22..e8763dc 100644
|
|
98
|
+
--- a/tools/harness_runtime.py
|
|
99
|
+
+++ b/tools/harness_runtime.py
|
|
100
|
+
@@ -67,6 +67,14 @@ SPECS = {
|
|
101
|
+
config_name="config.json",
|
|
102
|
+
docker_context=HARNESS_DOCKER_DIR / "nanobot",
|
|
103
|
+
),
|
|
104
|
+
+ "tether": HarnessSpec(
|
|
105
|
+
+ harness_id="tether",
|
|
106
|
+
+ version="0.2.5",
|
|
107
|
+
+ image="harnessbench/tether:0.2.5",
|
|
108
|
+
+ protocol="chat_completions",
|
|
109
|
+
+ config_name="providers.yaml",
|
|
110
|
+
+ docker_context=HARNESS_DOCKER_DIR / "tether",
|
|
111
|
+
+ ),
|
|
112
|
+
"goose": HarnessSpec(
|
|
113
|
+
harness_id="goose",
|
|
114
|
+
version="1.44.0",
|
|
115
|
+
@@ -291,6 +299,19 @@ def render_harness_config(
|
|
116
|
+
}
|
|
117
|
+
return json.dumps(config, ensure_ascii=False, indent=2) + "\n"
|
|
118
|
+
|
|
119
|
+
+ if spec.harness_id == "tether":
|
|
120
|
+
+ entry = {
|
|
121
|
+
+ "kind": "openai",
|
|
122
|
+
+ "base_url": base_url,
|
|
123
|
+
+ "api_key_env": "VLLM_API_KEY",
|
|
124
|
+
+ "context_tokens": context_window,
|
|
125
|
+
+ "fold_after": 8,
|
|
126
|
+
+ "timeout": 1800,
|
|
127
|
+
+ "params": {"temperature": temperature, "max_tokens": max_tokens},
|
|
128
|
+
+ }
|
|
129
|
+
+ header = f"# model: {model}\n# turns: {max_agent_turns}\n"
|
|
130
|
+
+ return header + yaml.safe_dump({"harnessbench": entry}, sort_keys=False)
|
|
131
|
+
+
|
|
132
|
+
if spec.harness_id == "goose":
|
|
133
|
+
config = {
|
|
134
|
+
"provider": "openai",
|
|
@@ -20,6 +20,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
20
20
|
TASK_CFG = { # tether tools / step cap / wall timeout
|
|
21
21
|
"code-simple": ("edit,bash", 30, 600), "code-medium": ("edit,bash", 40, 900), "code-hard": ("edit,bash", 50, 1200),
|
|
22
22
|
"focus": ("edit,bash,mem", 30, 600),
|
|
23
|
+
"plan": ("edit,bash", 20, 420), "codereview": ("edit,bash", 20, 420),
|
|
23
24
|
"qa": ("", 10, 300), "content": ("", 10, 300), "review": ("edit", 15, 420),
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Do not edit any file. Review inventory.py. Call done with a numbered list of the real defects you find: for each, name the function, say what is wrong and give the one-line fix. Report only real bugs, no style remarks.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Inventory helpers for a small shop."""
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def page(items, number, size=10):
|
|
6
|
+
"""Return page `number` (1-based) of `items`."""
|
|
7
|
+
start = number * size
|
|
8
|
+
return items[start:start + size]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_tag(item, tag, tags=[]):
|
|
12
|
+
tags.append(tag)
|
|
13
|
+
item["tags"] = tags
|
|
14
|
+
return item
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def total_price(lines):
|
|
18
|
+
total = 0.0
|
|
19
|
+
for line in lines:
|
|
20
|
+
total += line["price"] * line["qty"]
|
|
21
|
+
return total
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_free(price):
|
|
25
|
+
return price == 0.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_by_name(conn, name):
|
|
29
|
+
cur = conn.execute("SELECT id, name FROM items WHERE name = '" + name + "'")
|
|
30
|
+
return cur.fetchall()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_stock(path):
|
|
34
|
+
f = open(path)
|
|
35
|
+
data = f.read()
|
|
36
|
+
return dict(line.split(",") for line in data.splitlines())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def stock_of(stock, name):
|
|
40
|
+
return int(stock[name])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Do not edit any file. Read svc/client.py and svc/config.py, then call done with an implementation plan (a numbered list of 6 to 10 steps) for adding client-side retries with exponential backoff and jitter to the HTTP client. The plan must name the exact files and functions to change, say how the retry limit and base delay become settings, say which failures are retried (for example HTTP 429 and 5xx, timeouts) and which are not, say how Retry-After is honoured, and say how it will be tested without a real network. Plan only, no code.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""HTTP client used by every service call."""
|
|
2
|
+
import json
|
|
3
|
+
import urllib.request
|
|
4
|
+
|
|
5
|
+
from svc.config import BASE_URL, TIMEOUT
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get(path):
|
|
9
|
+
with urllib.request.urlopen(BASE_URL + path, timeout=TIMEOUT) as resp:
|
|
10
|
+
return json.loads(resp.read())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def post(path, payload):
|
|
14
|
+
req = urllib.request.Request(
|
|
15
|
+
BASE_URL + path, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"}
|
|
16
|
+
)
|
|
17
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
|
18
|
+
return json.loads(resp.read())
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v0.2.6 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
Found by using tether as its own SWE agent and by the v0.2.5 post-release check: a sandbox that refused harmless commands, sessions that only read, and a `query` that could not say why it found nothing. Closes #77, #93, #125, #127.
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- CLI: `tether query` now says why nothing matched (#93): `Error: unknown session` with exit code 1 for a wrong id, `No chunks are indexed for <id> yet` for an empty session, and the number of indexed chunks otherwise. `search_chunks` also tries a prefix match (`loop*`) after the AND and OR matches, so `loop` finds `looping`.
|
|
10
|
+
- Store: `SQLiteStore(":memory:")` no longer creates a file literally named `:memory:` in the working directory (#77). The path now maps to a temporary file outside the workspace, so every store method still shares one database.
|
|
11
|
+
- Sandbox: with `bwrap` active, `bash` no longer refuses commands by text for `..`, absolute paths outside the workspace, or `.env`/`.tether`/`.git/config` mentions (#125). The kernel mounts already enforce them (read-only root, hidden `$HOME`, masked secrets), so the text rules only produced false refusals such as `ls ~/..` or `cat /etc/hosts`. They remain the fallback when `bwrap` is missing, and `sudo`, `su`, disk tools, `rm -rf /`, `chmod 777`, fork bombs and `~/.ssh` stay refused in both modes.
|
|
12
|
+
- Sandbox: `.env.example`, `.env.sample`, `.env.template` and `.env.dist` are readable and writable; real `.env`, `.env.local`, `.env.production` stay masked and protected. Templates hold no secrets.
|
|
13
|
+
|
|
3
14
|
## v0.2.5 (2026-10-23)
|
|
4
15
|
|
|
5
16
|
Found by benchmarking the same models inside Tether and inside a plain tool loop (7 tasks, 10+ models): model idioms made the single-`act` grammar fail invisibly, and loops or oversized prompts burned steps and tokens. Closes #76, #81, #86, #89, #92 and the shell-idiom bugs #109, #110.
|
|
@@ -46,6 +57,8 @@ Dogfooding fixes: the four milestone bugs (#74 #75 #85 #88) plus the friction fo
|
|
|
46
57
|
|
|
47
58
|
### Fixes
|
|
48
59
|
|
|
60
|
+
- Harness: a session that only reads gets a deterministic hint (#127). After 12 consecutive read-only steps with no change at all, then every 8 more, the observation ends with `[NUDGE] No file changes in N steps. Make your edit now, or call `done` with what you found.` (0 static prompt tokens). Before, the quiet nudge started only after a first change, so a model that never edited (nemotron-3-ultra and glm-5.3 on scoped briefs: 40 steps, 150k to 215k prompt tokens, no change) was never told.
|
|
61
|
+
- Harness: `awk`/`gawk`/`mawk` count as read-only (unless they print to a file, pipe to a shell, call `system()` or edit in place) and as range-shift reads of one file, so the #92 stall guard sees `awk 'NR>=a && NR<=b' f` loops. `NR>=5` used to look like a redirect, also inside compound commands (`grep ...; awk 'NR>=5' f`), which silently turned the nudges off.
|
|
49
62
|
- Store: a `RUNNING` session whose worker pid no longer exists is flipped to `FAILED` (`Worker died (pid N) before finishing`) when read via `get_session`/`list_sessions`, so `status`, `list`, `wait` and `read` no longer report a killed worker as running forever (#85). A pid owned by another user counts as alive.
|
|
50
63
|
- Providers: `429` retries wait `Retry-After` (else 10s doubling, capped 30s) and get up to 5 attempts instead of 3 quick ones, so rate-limited hosted models (NVIDIA free tier) no longer fail the session after 3 quick attempts.
|
|
51
64
|
- Providers: each `post_with_retry` attempt is now bounded by a total `asyncio.wait_for(timeout)` deadline, so a trickling/stalled response cannot hang past the configured timeout; the deadline is retried like other transient errors and shows as `TimeoutError` (vs `ReadTimeout`) in the final `ProviderError` (#88).
|
|
@@ -85,7 +85,7 @@ trigger it, script the reply; if infrastructure must fail, add a fault to the pr
|
|
|
85
85
|
|
|
86
86
|
## Model conformance bench (developers only)
|
|
87
87
|
|
|
88
|
-
`bench/` holds
|
|
88
|
+
`bench/` holds nine tasks (the `focus` task checks that an exact single-file goal is not lost in a distractor-heavy repo) with hidden-test graders and three runners (Tether, a plain tool loop,
|
|
89
89
|
Claude Code) so the same model can be compared across harnesses and releases. It needs real
|
|
90
90
|
models, so it is not in CI and not in the package: use it when onboarding a model or before a
|
|
91
91
|
release to see friction that scripted tests cannot (the shell-wrapper, bare-command and
|
|
@@ -13,7 +13,7 @@ Tether never relies on system prompt instructions ("please do not read `.env` or
|
|
|
13
13
|
## Four Layers of Deterministic Defense
|
|
14
14
|
|
|
15
15
|
### 1. Canonical Path Confinement (`resolve_safe_path`)
|
|
16
|
-
Every path passed to `edit` or referenced in `bash` is canonicalized via `Path.resolve()` and verified to reside inside `workspace_root`. Traversal attempts (`../../etc/passwd`) raise `SandboxViolationError` before touching the filesystem.
|
|
16
|
+
Every path passed to `edit` or referenced in `bash` is canonicalized via `Path.resolve()` and verified to reside inside `workspace_root`. Traversal attempts (`../../etc/passwd`) raise `SandboxViolationError` before touching the filesystem. For `bash`, the kernel jail (below) is the boundary when `bwrap` is available: path and `.env` text checks on the command line run only in the no-`bwrap` fallback, so harmless commands such as `ls ~/..` are not refused for how they look. The cheap red lines (`sudo`, `su`, disk tools, `rm -rf /`, `chmod 777`, fork bombs, `~/.ssh`) are checked in both modes. `.env.example`, `.env.sample`, `.env.template` and `.env.dist` are not treated as secrets.
|
|
17
17
|
|
|
18
18
|
### 2. Protected Sensitive Paths
|
|
19
19
|
Even within `workspace_root`, Tether deterministically blocks access to sensitive files and directories:
|
|
File without changes
|
|
@@ -66,3 +66,20 @@ def test_budget_holds_with_a_wide_fold_window(tether: Tether, chaos) -> None:
|
|
|
66
66
|
assert d["rc"] == 0
|
|
67
67
|
for req in chaos.requests:
|
|
68
68
|
assert _size(req) <= 1500 * 1.3, f"prompt grew past the budget: ~{_size(req)} tokens"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_awk_ranges_of_one_file_count_as_range_shift_reads(tether: Tether, chaos) -> None:
|
|
72
|
+
(tether.ws / "big.py").write_text("x = 1\n" * 300)
|
|
73
|
+
reads = [f"bash awk 'NR>={i * 20 + 1} && NR<={i * 20 + 20}' big.py" for i in range(12)]
|
|
74
|
+
chaos.script(*reads)
|
|
75
|
+
d = tether.run("explore one file", "--tools", "bash", "--max-steps", "30")
|
|
76
|
+
assert any("reads of `big.py`" in o for o in tether.tool_outputs(d["id"]))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_explore_nudge_after_twelve_reads_without_a_change(tether: Tether, chaos) -> None:
|
|
80
|
+
for i in range(14):
|
|
81
|
+
(tether.ws / f"f{i}.txt").write_text(f"file {i}\n")
|
|
82
|
+
chaos.script(*[f"bash cat f{i}.txt" for i in range(14)], ("done", "ok"))
|
|
83
|
+
d = tether.run("read everything", "--tools", "bash", "--max-steps", "30")
|
|
84
|
+
outs = tether.tool_outputs(d["id"])
|
|
85
|
+
assert sum("[NUDGE] No file changes in 12 steps" in o for o in outs) == 1, outs[-3:]
|
|
@@ -3,6 +3,10 @@ result never depends on the model's obedience; tether alone must say no."""
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
6
10
|
from e2e.conftest import Tether
|
|
7
11
|
|
|
8
12
|
|
|
@@ -60,3 +64,14 @@ def test_verify_gate_can_run_a_tool_installed_under_home(tether: Tether, chaos)
|
|
|
60
64
|
chaos.script(("done", "ok"))
|
|
61
65
|
d = tether.run("x", "--verify", "gatetool", "--tools", "bash")
|
|
62
66
|
assert d["rc"] == 0, d
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.skipif(shutil.which("bwrap") is None, reason="needs bwrap")
|
|
70
|
+
def test_bash_reads_env_template_but_not_the_secret(tether: Tether, chaos) -> None:
|
|
71
|
+
(tether.ws / ".env").write_text("TOKEN=hunter2")
|
|
72
|
+
(tether.ws / ".env.example").write_text("TOKEN=\n")
|
|
73
|
+
chaos.script("bash cat .env.example", "bash cat .env", ("done", "ok"))
|
|
74
|
+
r = tether.run("read env files", "--tools", "bash")
|
|
75
|
+
template, secret = tether.tool_outputs(r["id"])[:2]
|
|
76
|
+
assert "TOKEN=" in template and "Sandbox violation" not in template, template
|
|
77
|
+
assert "hunter2" not in secret, secret # masked by the kernel mount, not by a text rule
|
|
@@ -156,3 +156,19 @@ async def test_stop_terminates_a_running_worker(
|
|
|
156
156
|
assert await _async_main(["stop", rec.id, "--json", "--db", db_arg]) == 0
|
|
157
157
|
assert '"kind": "stopped"' in capsys.readouterr().out
|
|
158
158
|
assert len(killed) == 1 # a finished session is left alone
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def test_query_reports_why_nothing_matched(
|
|
162
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
163
|
+
) -> None:
|
|
164
|
+
db_arg = str(tmp_path / "tether.db")
|
|
165
|
+
store = SQLiteStore(db_path=db_arg)
|
|
166
|
+
rec = store.create_session("g")
|
|
167
|
+
assert await _async_main(["query", "ses_nope", "x", "--db", db_arg]) == 1
|
|
168
|
+
assert "unknown session `ses_nope`" in capsys.readouterr().err
|
|
169
|
+
assert await _async_main(["query", rec.id, "anything", "--db", db_arg]) == 0
|
|
170
|
+
assert "No chunks are indexed for" in capsys.readouterr().out
|
|
171
|
+
store.add_observation(rec.id, step=1, cmd="bash x", raw_output="alpha beta gamma")
|
|
172
|
+
assert await _async_main(["query", rec.id, "nonexistent_xyz", "--db", db_arg]) == 0
|
|
173
|
+
out = capsys.readouterr().out
|
|
174
|
+
assert "1 chunks indexed" in out and "nonexistent_xyz" in out
|
|
@@ -11,7 +11,7 @@ import pytest
|
|
|
11
11
|
from tests.conftest import FakeScriptedProvider
|
|
12
12
|
from tether import ActionCall, Harness, ModelResponse, register_provider, tool
|
|
13
13
|
from tether.decorator import ToolSpec
|
|
14
|
-
from tether.harness import detect_cycle, read_target, spawn_detached_worker
|
|
14
|
+
from tether.harness import detect_cycle, is_quiet, read_target, spawn_detached_worker
|
|
15
15
|
from tether.store import SQLiteStore
|
|
16
16
|
|
|
17
17
|
|
|
@@ -662,3 +662,59 @@ async def test_context_overflow_warns_once_on_stderr(
|
|
|
662
662
|
err = capsys.readouterr().err
|
|
663
663
|
assert err.count("over --context-tokens 20") == 1
|
|
664
664
|
assert "raise the budget" in err
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
async def test_explore_nudge_when_nothing_changes(tmp_path: Path) -> None:
|
|
668
|
+
run = await _run_commands(tmp_path, [f"bash cat f{i}" for i in range(14)]) # no change at all
|
|
669
|
+
obs = run.provider.store.list_observations(run.session.id)
|
|
670
|
+
steps = [o.step for o in obs.values() if "[NUDGE] No file changes in 12 steps" in o.raw_output]
|
|
671
|
+
assert steps == [12]
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
async def test_explore_nudge_repeats_while_the_model_only_reads(
|
|
675
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
676
|
+
) -> None:
|
|
677
|
+
monkeypatch.setattr("tether.harness.EXPLORE_NUDGE_STEPS", 2)
|
|
678
|
+
monkeypatch.setattr("tether.harness.EXPLORE_NUDGE_EVERY", 3)
|
|
679
|
+
run = await _run_commands(tmp_path, [f"bash cat f{i}" for i in range(9)])
|
|
680
|
+
obs = run.provider.store.list_observations(run.session.id)
|
|
681
|
+
assert [o.step for o in obs.values() if "[NUDGE] No file changes" in o.raw_output] == [2, 5, 8]
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
async def test_explore_nudge_is_off_after_a_change(
|
|
685
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
686
|
+
) -> None:
|
|
687
|
+
monkeypatch.setattr("tether.harness.EXPLORE_NUDGE_STEPS", 2)
|
|
688
|
+
run = await _run_commands(tmp_path, ["ping a.com", "bash cat f0", "bash cat f1", "bash cat f2"])
|
|
689
|
+
obs = run.provider.store.list_observations(run.session.id)
|
|
690
|
+
assert not [o for o in obs.values() if "[NUDGE] No file changes in" in o.raw_output]
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
@pytest.mark.parametrize(
|
|
694
|
+
("cmd", "quiet"),
|
|
695
|
+
[
|
|
696
|
+
("awk 'NR>=470 && NR<=500 {printf \"%d:%s\\n\", NR, $0}' tether/cli.py", True),
|
|
697
|
+
("bash awk '/a/{n++} END{print n}' README.md", True),
|
|
698
|
+
("awk '{print $1 > \"out.txt\"}' f", False),
|
|
699
|
+
("awk 'BEGIN{system(\"rm x\")}'", False),
|
|
700
|
+
("awk '{print}' f | tee g", False),
|
|
701
|
+
("gawk -i inplace '{print}' f", False),
|
|
702
|
+
],
|
|
703
|
+
)
|
|
704
|
+
def test_awk_is_quiet_unless_it_writes(cmd: str, quiet: bool) -> None:
|
|
705
|
+
assert is_quiet(cmd) is quiet
|
|
706
|
+
assert (read_target(cmd) is not None) is quiet
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
@pytest.mark.parametrize(
|
|
710
|
+
("cmd", "quiet"),
|
|
711
|
+
[
|
|
712
|
+
("grep -n x f; awk 'NR>=820 && NR<=856' f", True), # `>=` is a comparison, not a redirect
|
|
713
|
+
("cat f | awk 'NR>5'", True),
|
|
714
|
+
("echo hi > out.txt", False),
|
|
715
|
+
("cat a >> b", False),
|
|
716
|
+
("ls >out", False),
|
|
717
|
+
],
|
|
718
|
+
)
|
|
719
|
+
def test_comparison_operators_are_not_redirects(cmd: str, quiet: bool) -> None:
|
|
720
|
+
assert is_quiet(cmd) is quiet
|
|
@@ -100,7 +100,7 @@ async def test_web_fetch_blocks_private_addresses(
|
|
|
100
100
|
],
|
|
101
101
|
)
|
|
102
102
|
def test_program_args_are_not_paths(cmd: str, tmp_path: Path) -> None:
|
|
103
|
-
SandboxPolicy(workspace_root=tmp_path).validate_bash_command(cmd)
|
|
103
|
+
SandboxPolicy(workspace_root=tmp_path, use_kernel_sandbox=False).validate_bash_command(cmd)
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
@pytest.mark.parametrize(
|
|
@@ -108,10 +108,61 @@ def test_program_args_are_not_paths(cmd: str, tmp_path: Path) -> None:
|
|
|
108
108
|
["sed -n p /etc/hosts", "awk -f /etc/passwd x", "cat /etc/hosts", "cd . && cat /etc/hosts"],
|
|
109
109
|
)
|
|
110
110
|
def test_real_absolute_paths_still_blocked(cmd: str, tmp_path: Path) -> None:
|
|
111
|
+
with pytest.raises(SandboxViolationError): # text rules are the no-bwrap fallback
|
|
112
|
+
SandboxPolicy(workspace_root=tmp_path, use_kernel_sandbox=False).validate_bash_command(cmd)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.parametrize(
|
|
116
|
+
"cmd",
|
|
117
|
+
["ls ~/..", "cat /etc/hosts", "cat .tether/x", "cat .git/config"],
|
|
118
|
+
)
|
|
119
|
+
def test_kernel_sandbox_leaves_path_policing_to_the_mounts(
|
|
120
|
+
cmd: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
121
|
+
) -> None:
|
|
122
|
+
monkeypatch.setattr(sb_mod, "_can_use_bwrap", lambda: True)
|
|
123
|
+
SandboxPolicy(workspace_root=tmp_path).validate_bash_command(cmd) # no false refusal
|
|
124
|
+
monkeypatch.setattr(sb_mod, "_can_use_bwrap", lambda: False)
|
|
125
|
+
with pytest.raises(SandboxViolationError): # same commands stay refused without bwrap
|
|
126
|
+
SandboxPolicy(workspace_root=tmp_path).validate_bash_command(cmd)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.parametrize("cmd", ["sudo ls", "rm -rf /", "cat ~/.ssh/id_rsa", "chmod 777 x"])
|
|
130
|
+
def test_kernel_sandbox_keeps_the_cheap_red_lines(
|
|
131
|
+
cmd: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
132
|
+
) -> None:
|
|
133
|
+
monkeypatch.setattr(sb_mod, "_can_use_bwrap", lambda: True)
|
|
111
134
|
with pytest.raises(SandboxViolationError):
|
|
112
135
|
SandboxPolicy(workspace_root=tmp_path).validate_bash_command(cmd)
|
|
113
136
|
|
|
114
137
|
|
|
138
|
+
@pytest.mark.parametrize(
|
|
139
|
+
("name", "secret"),
|
|
140
|
+
[(".env", True), (".env.local", True), (".env.example", False), (".env.dist", False)],
|
|
141
|
+
)
|
|
142
|
+
def test_env_templates_are_readable_secrets_are_not(
|
|
143
|
+
name: str, secret: bool, tmp_path: Path
|
|
144
|
+
) -> None:
|
|
145
|
+
(tmp_path / name).write_text("K=v")
|
|
146
|
+
sp = SandboxPolicy(workspace_root=tmp_path, use_kernel_sandbox=False)
|
|
147
|
+
if secret:
|
|
148
|
+
with pytest.raises(SandboxViolationError):
|
|
149
|
+
sp.resolve_safe_path(name)
|
|
150
|
+
with pytest.raises(SandboxViolationError):
|
|
151
|
+
sp.validate_bash_command(f"cat {name}")
|
|
152
|
+
else:
|
|
153
|
+
assert sp.resolve_safe_path(name).name == name
|
|
154
|
+
sp.validate_bash_command(f"cat {name}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@needs_bwrap
|
|
158
|
+
async def test_masked_secret_and_env_example_under_real_bwrap(tmp_path: Path) -> None:
|
|
159
|
+
(tmp_path / ".env").write_text("TOKEN=secret")
|
|
160
|
+
(tmp_path / ".env.example").write_text("TOKEN=\n")
|
|
161
|
+
sp = SandboxPolicy(workspace_root=tmp_path)
|
|
162
|
+
code, out = await sp.execute_bash_with_code("cat .env.example; echo ---; cat .env")
|
|
163
|
+
assert "TOKEN=" in out.split("---")[0] and "secret" not in out, out
|
|
164
|
+
|
|
165
|
+
|
|
115
166
|
def test_unknown_tool_name_rejected(tmp_path: Path) -> None:
|
|
116
167
|
with pytest.raises(ValueError, match="bahs"):
|
|
117
168
|
Harness(tools="bash,bahs", db_path=tmp_path / "t.db")
|
|
@@ -314,3 +314,26 @@ def test_reap_dead_keeps_other_users_pid(tmp_path: Path, monkeypatch: pytest.Mon
|
|
|
314
314
|
monkeypatch.setattr("os.kill", deny)
|
|
315
315
|
got = store.get_session(rec.id)
|
|
316
316
|
assert got is not None and got.status == "RUNNING"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_count_chunks_and_prefix_fallback(tmp_path: Path) -> None:
|
|
320
|
+
store = SQLiteStore(db_path=tmp_path / "t.db")
|
|
321
|
+
rec = store.create_session("g")
|
|
322
|
+
assert store.count_chunks([]) == 0
|
|
323
|
+
assert store.count_chunks([rec.id]) == 0
|
|
324
|
+
store.add_observation(rec.id, step=1, cmd="bash x", raw_output="the model keeps looping here")
|
|
325
|
+
assert store.count_chunks([rec.id]) >= 1
|
|
326
|
+
hits = store.search_chunks("loop", session_ids=[rec.id]) # prefix: loop -> looping
|
|
327
|
+
assert hits and "looping" in hits[0].content
|
|
328
|
+
assert store.search_chunks("zz", session_ids=[rec.id]) == [] # too short for a prefix match
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_memory_path_does_not_create_a_file_in_cwd(
|
|
332
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
333
|
+
) -> None:
|
|
334
|
+
monkeypatch.chdir(tmp_path)
|
|
335
|
+
store = SQLiteStore(":memory:")
|
|
336
|
+
rec = store.create_session("g")
|
|
337
|
+
assert store.get_session(rec.id) is not None
|
|
338
|
+
assert not (tmp_path / ":memory:").exists()
|
|
339
|
+
assert store.db_path.parent != tmp_path
|