interlockd 0.1.2__tar.gz → 0.1.4__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.
- interlockd-0.1.4/.github/CODEOWNERS +2 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/.github/pull_request_template.md +1 -0
- interlockd-0.1.4/.github/workflows/ci.yml +134 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/CHANGELOG.md +15 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/CONTRIBUTING.md +12 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/PKG-INFO +7 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/README.md +6 -4
- interlockd-0.1.4/adapters/gemini-cli/PAYLOADS.md +65 -0
- interlockd-0.1.4/adapters/windsurf/PAYLOADS.md +49 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/external/README.md +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/GETTING-STARTED.md +2 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/USER-GUIDE.md +6 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/github-workflow.yml +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/__init__.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/api.py +37 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/auth.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/bench.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/capture.py +24 -8
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/cli.py +56 -47
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/config.py +3 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/control.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/corpus.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/describe.py +1 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/flow.py +38 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/mandate.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/memory.py +6 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/packs.py +21 -16
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/gateway.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/insights.py +12 -9
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/client.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/judge_setup.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/judges/local_probe.py +28 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/mcp_http.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/mcp_proxy.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/normalize/gemini_cli.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/normalize/more.py +5 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/ogr.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/onboard.py +18 -18
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/otel.py +2 -2
- interlockd-0.1.4/interlockd/paths.py +96 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/pipeline.py +50 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/policy/__init__.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/policy/workspace.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/scan.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/service.py +72 -13
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/shims/claude-code/interlock-hook.sh +4 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/shims/kiro/interlock-hook.mjs +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/shims/kiro/interlock-hook.sh +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/store/spool.py +30 -9
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/testkit.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/transcript/kiro.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/pyproject.toml +1 -1
- interlockd-0.1.4/scripts/check-hygiene.sh +26 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/sdk/js/package.json +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/sdk/js/src/index.js +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/conftest.py +24 -2
- interlockd-0.1.4/tests/fixtures/gemini-cli/post-read-benign.json +15 -0
- interlockd-0.1.4/tests/fixtures/gemini-cli/post-read-inject.json +15 -0
- interlockd-0.1.4/tests/fixtures/gemini-cli/post-shell-benign.json +16 -0
- interlockd-0.1.4/tests/fixtures/gemini-cli/post-shell-rot13-inject.json +16 -0
- interlockd-0.1.4/tests/fixtures/gemini-cli/pre-read-benign.json +11 -0
- interlockd-0.1.4/tests/fixtures/gemini-cli/pre-shell-benign.json +12 -0
- interlockd-0.1.4/tests/fixtures/gemini-cli/pre-shell-block.json +12 -0
- interlockd-0.1.4/tests/fixtures/windsurf/post-shell-benign.json +10 -0
- interlockd-0.1.4/tests/fixtures/windsurf/pre-shell-benign.json +10 -0
- interlockd-0.1.4/tests/fixtures/windsurf/pre-shell-block.json +10 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_aibom.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_audit_chain.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_calibration.py +12 -12
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_capture.py +37 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_claude_agent_sdk.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_claude_code.py +21 -15
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_control.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_cursor_gemini.py +23 -10
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_examples.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_gateway.py +19 -18
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_insights.py +7 -6
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_interlock_test.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_js_sdk.py +6 -6
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_judge.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_judge_setup.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_local_probe.py +34 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_managed_policy.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_mcp_policy.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_mcp_proxy.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_mcp_server.py +11 -9
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_more_adapters.py +65 -10
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_ogr.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_onboard.py +19 -17
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_opencode.py +5 -4
- interlockd-0.1.4/tests/test_paths_windows.py +64 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_repo_policy.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_sandbox.py +8 -8
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_scan.py +14 -12
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_shim_sh.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_spool.py +1 -1
- interlockd-0.1.4/tests/test_text_encoding.py +28 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_transcript.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_unix_socket.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_version.py +8 -4
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_watch.py +10 -10
- interlockd-0.1.2/.github/workflows/ci.yml +0 -66
- interlockd-0.1.2/adapters/gemini-cli/PAYLOADS.md +0 -36
- interlockd-0.1.2/adapters/windsurf/PAYLOADS.md +0 -15
- interlockd-0.1.2/interlockd/paths.py +0 -63
- {interlockd-0.1.2 → interlockd-0.1.4}/.github/ISSUE_TEMPLATE/agent.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/.github/ISSUE_TEMPLATE/decision.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/.github/dependabot.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/.github/workflows/release.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/.gitignore +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/CODE_OF_CONDUCT.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/LICENSE +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/NOTICE +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/SECURITY.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/action.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/antigravity/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/claude-code/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/claude-code/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/codex/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/codex/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/copilot-cli/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/cursor/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/kiro/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/adapters/opencode/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/benchmarks/agentdojo_template.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/benchmarks/injecagent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/benchmarks/nl2bash.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/_corpus.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/_corpus.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/exfil_and_secrets.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/git_and_history.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/injection_persistence_supplychain.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/judge_confirmation.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/judge_exfil.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/judge_injection.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/judge_intent.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/archetypes/slow_hijack.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/external/samples/agentdojo.sample.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/external/samples/agentdrift.sample.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/external/samples/injecagent.sample.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/ogr-seed-v0/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/corpus/ogr-seed-v0/seed-v0.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/demo/demo.sh +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/demo/demo.tape +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/ADAPTERS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/BENCHMARKS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/PERFORMANCE.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/THREAT-MODEL.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/mandate-example.cedar +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/policy-example.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/reports/M3-judge-calibration.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/reports/ogr-seed-v0-results.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/docs/reports/ogr-seed-v0-results.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/claude_agent_sdk_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/crewai_crew.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/google_adk_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/langgraph_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/openai_agents.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/semantic_kernel_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/agents/strands_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/policies/ci.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/policies/mcp-locked-down.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/examples/policies/strict-team.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/__main__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/aibom.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/calibration.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/compose.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/_corpus.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/egress.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/fileops.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/git.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/packages.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/persistence.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/secrets.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/data/packs/shell.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/engine.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/mcp.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/oauth.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/det/shellparse.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/egress.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/ids.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/claude_agent_sdk.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/crewai.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/google_adk.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/langchain.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/openai_agents.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/semantic_kernel.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/integrations/strands.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/judges/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/judges/guardrails.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/judges/projector.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/judges/strands_decider.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/ledger.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/mcp_server.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/models.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/normalize/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/normalize/claude_code.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/normalize/cursor.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/normalize/kiro.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/plugins.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/policy/managed.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/policy_lint.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/render.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/sandbox.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/schema/verdict.schema.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/selftest.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/shims/opencode/interlock.js +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/store/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/store/drainer.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/store/dynamodb.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/store/memory.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/tiers.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/transcript/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/transcript/claude_code.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/interlockd/watch.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/packaging/homebrew/Formula/interlock.rb +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/packaging/homebrew/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/packaging/homebrew/make_formula.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/sdk/js/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/sdk/js/src/index.d.ts +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fake_mcp_server.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/claude-code/post-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/claude-code/post-read-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/claude-code/pre-bash-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/claude-code/pre-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/codex/post-bash-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/codex/post-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/codex/pre-bash-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/codex/pre-bash-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/copilot-cli/post-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/copilot-cli/pre-bash-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/copilot-cli/pre-bash-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/cursor/post-read-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/cursor/post-shell-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/cursor/pre-read-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/cursor/pre-shell-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/fixtures/cursor/pre-shell-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_api.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_bench.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_corpus.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_crewai.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_dynamodb.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_egress.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_flow.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_google_adk.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_homebrew_formula.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_langchain.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_mandate.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_mcp_http.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_mcp_oauth.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_memory.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_normalize_kiro.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_openai_agents.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_otel.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_packs.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_pipeline.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_plugins.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_policy_lint.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_selftest.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_semantic_kernel.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_strands.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.4}/tests/test_tiers.py +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
hygiene:
|
|
13
|
+
# nothing identifying a real machine, account or person; no private-tooling leftovers (scripts/check-hygiene.sh)
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- run: scripts/check-hygiene.sh
|
|
18
|
+
|
|
19
|
+
dco:
|
|
20
|
+
# every commit in a pull request carries a Developer Certificate of Origin sign-off (git commit -s); bots exempt
|
|
21
|
+
if: github.event_name == 'pull_request'
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v7
|
|
25
|
+
with: { fetch-depth: 0 }
|
|
26
|
+
- name: Signed-off-by on every commit
|
|
27
|
+
env:
|
|
28
|
+
BASE: ${{ github.event.pull_request.base.sha }}
|
|
29
|
+
HEAD: ${{ github.event.pull_request.head.sha }}
|
|
30
|
+
run: |
|
|
31
|
+
commits=$(git rev-list "$BASE..$HEAD") # fails the job if the range can't be read: never pass unchecked
|
|
32
|
+
bad=0
|
|
33
|
+
for c in $commits; do
|
|
34
|
+
case "$(git log -1 --format=%ae "$c")" in *"[bot]@users.noreply.github.com") continue ;; esac
|
|
35
|
+
if ! git log -1 --format=%B "$c" | grep -qE '^Signed-off-by: .+ <.+>'; then
|
|
36
|
+
echo "::error::$(git log -1 --format='%h %s' "$c"): no Signed-off-by line. Fix with: git rebase --signoff $BASE && git push --force-with-lease"
|
|
37
|
+
bad=1
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
exit $bad
|
|
41
|
+
|
|
42
|
+
windows:
|
|
43
|
+
# native Windows: daemon, policy, audit, scan, SDKs (agent hooks and the sandbox stay macOS/Linux; WSL2)
|
|
44
|
+
runs-on: windows-latest
|
|
45
|
+
defaults:
|
|
46
|
+
run:
|
|
47
|
+
shell: bash
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v7
|
|
50
|
+
- uses: astral-sh/setup-uv@v7
|
|
51
|
+
- name: Install
|
|
52
|
+
run: |
|
|
53
|
+
uv venv --python 3.12
|
|
54
|
+
uv pip install -e ".[dev]" --python .venv/Scripts/python.exe
|
|
55
|
+
- name: Unit tests
|
|
56
|
+
run: .venv/Scripts/python.exe -m pytest -q -p no:cacheprovider
|
|
57
|
+
- name: Rule corpus gate
|
|
58
|
+
run: .venv/Scripts/interlock.exe test
|
|
59
|
+
- name: Live daemon on Windows (start, decide, audit, stop)
|
|
60
|
+
run: |
|
|
61
|
+
export INTERLOCK_HOME="$RUNNER_TEMP/ih" IL=.venv/Scripts/interlock.exe
|
|
62
|
+
$IL enforce && $IL start && $IL status
|
|
63
|
+
.venv/Scripts/python.exe - <<'PY'
|
|
64
|
+
import os
|
|
65
|
+
from interlockd.integrations.client import InterlockClient
|
|
66
|
+
c = InterlockClient()
|
|
67
|
+
cases = [("Bash", {"command": "git push --force origin main"}, "block"),
|
|
68
|
+
("Write", {"file_path": os.path.expanduser(r"~\.CLAUDE\settings.json"), "content": "{}"}, "block"),
|
|
69
|
+
("Read", {"file_path": "README.md"}, "allow")]
|
|
70
|
+
for tool, args, want in cases:
|
|
71
|
+
v = c.decide("pre_action", tool, args, cwd=os.getcwd())
|
|
72
|
+
print(tool, args, "->", v and v["disposition"])
|
|
73
|
+
assert v and v["disposition"] == want, v
|
|
74
|
+
PY
|
|
75
|
+
$IL audit verify
|
|
76
|
+
$IL stop
|
|
77
|
+
sleep 2
|
|
78
|
+
if curl -sf http://127.0.0.1:7391/interlock/v1/health; then echo "daemon still running after stop"; exit 1; fi
|
|
79
|
+
|
|
80
|
+
test:
|
|
81
|
+
runs-on: ${{ matrix.os }}
|
|
82
|
+
strategy:
|
|
83
|
+
matrix:
|
|
84
|
+
os: [ubuntu-latest, macos-latest]
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v7
|
|
87
|
+
- uses: astral-sh/setup-uv@v7
|
|
88
|
+
- name: Install
|
|
89
|
+
run: |
|
|
90
|
+
uv venv --python 3.12
|
|
91
|
+
uv pip install -e ".[dev]" --python .venv/bin/python
|
|
92
|
+
- name: Lint
|
|
93
|
+
run: .venv/bin/ruff check .
|
|
94
|
+
- name: Unit tests
|
|
95
|
+
run: .venv/bin/pytest -q
|
|
96
|
+
- name: Rule corpus gate
|
|
97
|
+
run: .venv/bin/interlock test
|
|
98
|
+
- name: Evaluation corpus gate
|
|
99
|
+
run: .venv/bin/interlock test --corpus corpus/archetypes
|
|
100
|
+
- name: Linux sandbox (bubblewrap)
|
|
101
|
+
if: runner.os == 'Linux'
|
|
102
|
+
run: |
|
|
103
|
+
sudo apt-get install -y -qq bubblewrap
|
|
104
|
+
# Ubuntu 24.04 restricts unprivileged user namespaces through AppArmor; bubblewrap needs them
|
|
105
|
+
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
|
106
|
+
.venv/bin/pytest -q tests/test_sandbox.py -rs
|
|
107
|
+
- name: Framework integrations against a real daemon
|
|
108
|
+
run: |
|
|
109
|
+
uv pip install --python .venv/bin/python strands-agents langchain-core openai-agents google-adk claude-agent-sdk semantic-kernel
|
|
110
|
+
.venv/bin/pytest -q tests/test_strands.py tests/test_langchain.py tests/test_openai_agents.py tests/test_google_adk.py tests/test_claude_agent_sdk.py tests/test_semantic_kernel.py
|
|
111
|
+
- name: CrewAI integration (own environment)
|
|
112
|
+
run: |
|
|
113
|
+
uv venv /tmp/crewai --python 3.12
|
|
114
|
+
uv pip install --python /tmp/crewai/bin/python -e ".[dev]" crewai
|
|
115
|
+
/tmp/crewai/bin/pytest -q tests/test_crewai.py
|
|
116
|
+
- name: Wheel smoke test
|
|
117
|
+
run: |
|
|
118
|
+
uv build --wheel -o dist
|
|
119
|
+
uv venv /tmp/wheel --python 3.12
|
|
120
|
+
uv pip install --python /tmp/wheel/bin/python dist/*.whl
|
|
121
|
+
cd /tmp && /tmp/wheel/bin/interlock test
|
|
122
|
+
|
|
123
|
+
action:
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@v7
|
|
127
|
+
- name: Plant a risky MCP config to prove the scan sees it
|
|
128
|
+
run: |
|
|
129
|
+
mkdir -p .vscode
|
|
130
|
+
echo '{"servers":{"sh":{"command":"bash","args":["-c","node s.js"]}}}' > .vscode/mcp.json
|
|
131
|
+
- uses: ./
|
|
132
|
+
with: { fail-on: never, test-policy: "true" }
|
|
133
|
+
- name: SARIF has the finding
|
|
134
|
+
run: python3 -c "import json;r=json.load(open('interlock.sarif'))['runs'][0]['results'];print(len(r),'results');assert any(x['ruleId']=='mcp.inline_shell' for x in r)"
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
- Windsurf adapter verified live on Windsurf (Devin.app) 3.10.67: a refused force push, an injected README refused before Cascade read it, and a `cat` of it refused (`flow.reread_withheld`). Windsurf's read hooks carry no contents, so the daemon reads the file itself (256 KiB, never a sensitive path). Fixed live: Windsurf sends `cwd: null`, so a repository policy never applied; the hook's own directory is used now. Windsurf runs each hook twice; the repeat is answered from the first decision.
|
|
6
|
+
- `interlock capture` pairs Windsurf's calls with their results (its report showed a false "block held: no").
|
|
7
|
+
- Contributing: pull requests are reviewed and merged in this repository, with a DCO sign-off on each commit (`git commit -s`, checked in CI); CI also runs `scripts/check-hygiene.sh`, which keeps real home paths, account ids and keys out of the repository.
|
|
8
|
+
|
|
9
|
+
## 0.1.3 (2026-10-24)
|
|
10
|
+
|
|
11
|
+
- **Security fix (macOS, Windows):** path rules now match without case where the file system ignores it. On macOS (default APFS) a write to `~/.CLAUDE/settings.json`, `.GIT/hooks/pre-commit` or `.GitHub/Workflows/ci.yml` changes the real file but was allowed. Linux is unchanged.
|
|
12
|
+
- Native Windows, first part: the daemon (`interlock start | stop | status`, a detached background process), policy, audit log, `scan`, `report`, `explain`, `test` and the Python and JavaScript SDKs run on Windows without WSL2. Paths are matched the same way on every OS (`C:\Users\me\.claude\settings.json` hits the same rules as `~/.claude/settings.json`), and the Startup folder and PowerShell profiles count as persistence. Agent hooks, `interlock init`, the gateway, the sandbox and the local judge still need macOS or Linux (on Windows, WSL2); those commands say so. Verified on a Windows CI runner: the full test suite, and a live daemon (start, decisions through the Python client, `audit verify`, stop).
|
|
13
|
+
- Gemini CLI adapter verified live on Gemini CLI 0.65.0: a refused force push and a withheld injected README; captured payloads replayed in tests.
|
|
14
|
+
- Withheld content can't be read back another way: the local probe also scores ROT13 and base64 decodings of each tool result, and once a file is withheld, a shell call naming it in the same session asks (`flow.reread_withheld`). Found live: Gemini CLI re-read a withheld README through ROT13 "without triggering policy".
|
|
15
|
+
- `interlock capture`: `--scratch` projects push nowhere; for agents that send no tool-call id (Gemini CLI), the report pairs a refused call with a later result by session and input.
|
|
16
|
+
- Text files are read and written as UTF-8 on every platform.
|
|
17
|
+
|
|
3
18
|
## 0.1.2 (2026-10-24)
|
|
4
19
|
|
|
5
20
|
- Managed policy: `<interlock home>/managed-policy.yaml` is applied last and can only tighten decisions (it can switch enforce on, never loosen anything). It is reloaded without a restart; a file that doesn't parse is ignored and reported by `interlock policy lint`. For device management and centrally managed machines.
|
|
@@ -12,7 +12,8 @@ uv venv --python 3.12 && uv pip install -e ".[dev]" --python .venv/bin/python
|
|
|
12
12
|
.venv/bin/interlock test --corpus corpus/archetypes # evaluation corpus
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
CI runs all four on every pull request.
|
|
15
|
+
CI runs all four on every pull request, on Linux, macOS and Windows, plus `scripts/check-hygiene.sh` (no real home
|
|
16
|
+
paths, account ids or keys in committed files; run it before you push).
|
|
16
17
|
|
|
17
18
|
## What to contribute
|
|
18
19
|
|
|
@@ -22,12 +23,18 @@ CI runs all four on every pull request.
|
|
|
22
23
|
|
|
23
24
|
## Pull requests
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`Co-authored-by` on the published commit); your PR is closed with a link to it rather than merged with the button.
|
|
26
|
+
Fork, branch, and open a pull request against `main`. A maintainer reviews it; when CI is green and the review is
|
|
27
|
+
done, it is merged here.
|
|
28
28
|
|
|
29
|
+
- **Sign off every commit** (`git commit -s`). This adds `Signed-off-by: Your Name <you@example.com>` and certifies the
|
|
30
|
+
[Developer Certificate of Origin](https://developercertificate.org/): that you wrote the change or otherwise have the
|
|
31
|
+
right to submit it under this project's license. CI checks it; to fix a branch, `git rebase --signoff main` and
|
|
32
|
+
force-push. There is no separate contributor agreement.
|
|
33
|
+
- Contributions are licensed under Apache-2.0, the same license as the project (inbound = outbound). The project
|
|
34
|
+
stays Apache-2.0.
|
|
29
35
|
- Keep changes focused; include tests.
|
|
30
36
|
- Describe what you verified and how (live against the tool, or tests only).
|
|
31
|
-
-
|
|
37
|
+
- **Security issues**: don't open a pull request or issue; report privately (see [SECURITY.md](SECURITY.md)). Fixes
|
|
38
|
+
are prepared in a private security advisory and released before details are public.
|
|
32
39
|
|
|
33
40
|
By taking part you agree to the [code of conduct](CODE_OF_CONDUCT.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: interlockd
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Interlock daemon: a pre/post-action interlock for coding agents
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -56,8 +56,8 @@ pipx install interlockd # or: uv tool install interlockd, or: brew install i
|
|
|
56
56
|
interlock init # finds your agents, installs hooks, starts the daemon (shadow mode)
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Runs on macOS and Linux (Python 3.12+). On Windows, use it inside WSL2 and run your agents from the WSL terminal
|
|
60
|
-
|
|
59
|
+
Runs on macOS and Linux (Python 3.12+). On Windows, use it inside WSL2 and run your agents from the WSL terminal.
|
|
60
|
+
Natively on Windows, the daemon and the Python and JavaScript SDKs work today (`interlock start`); agent hooks are next.
|
|
61
61
|
|
|
62
62
|
New here? [docs/GETTING-STARTED.md](docs/GETTING-STARTED.md) walks through shadow mode, enforcing, MCP servers, your own agents and CI in about ten minutes.
|
|
63
63
|
|
|
@@ -79,7 +79,9 @@ What the gate decides (enforce mode, nothing executed):
|
|
|
79
79
|
| Kiro (IDE) | ✅ verified live |
|
|
80
80
|
| Cursor (CLI) | ✅ verified live: blocks before execution |
|
|
81
81
|
| GitHub Copilot CLI | ✅ verified live: blocks before execution, withholds injected tool results |
|
|
82
|
-
| Gemini CLI
|
|
82
|
+
| Gemini CLI | ✅ verified live: blocks before execution, withholds injected tool results |
|
|
83
|
+
| Windsurf (Devin.app) | ✅ verified live: blocks before execution, refuses to read injected files |
|
|
84
|
+
| Antigravity CLI, OpenCode | built from their documented hooks/plugins; live verification in progress |
|
|
83
85
|
| Your own agents (Claude Agent SDK, Strands, LangChain / LangGraph, OpenAI Agents SDK, Google ADK, CrewAI, Semantic Kernel) | ✅ in-process: `guard_options(options)`, `InterlockHooks()`, `guard_tools(tools)`, `guard_agent_tools(agent)`, `guard_agent(agent)`, `crewai.install()`, `guard_kernel(kernel)` |
|
|
84
86
|
| JavaScript / TypeScript agents (Vercel AI SDK, Claude Agent SDK for TS, Mastra) | ✅ `@interlockd/sdk`: `guardTools(tools)`, `guardOptions(options)` ([sdk/js](sdk/js)) |
|
|
85
87
|
| Any MCP client | ✅ the MCP gateway (`interlock gateway wrap`) gates every stdio and remote (HTTP) MCP server, hooks or not |
|
|
@@ -129,7 +131,7 @@ The full threat model is in [docs/THREAT-MODEL.md](docs/THREAT-MODEL.md). Latenc
|
|
|
129
131
|
|
|
130
132
|
```yaml
|
|
131
133
|
- uses: actions/checkout@v7
|
|
132
|
-
- uses: interlockd/interlock@v0.1.
|
|
134
|
+
- uses: interlockd/interlock@v0.1.4 # scan MCP/agent configs, test .interlock/policy.yaml
|
|
133
135
|
with: { fail-on: high }
|
|
134
136
|
- uses: github/codeql-action/upload-sarif@v3
|
|
135
137
|
if: always()
|
|
@@ -12,8 +12,8 @@ pipx install interlockd # or: uv tool install interlockd, or: brew install i
|
|
|
12
12
|
interlock init # finds your agents, installs hooks, starts the daemon (shadow mode)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Runs on macOS and Linux (Python 3.12+). On Windows, use it inside WSL2 and run your agents from the WSL terminal
|
|
16
|
-
|
|
15
|
+
Runs on macOS and Linux (Python 3.12+). On Windows, use it inside WSL2 and run your agents from the WSL terminal.
|
|
16
|
+
Natively on Windows, the daemon and the Python and JavaScript SDKs work today (`interlock start`); agent hooks are next.
|
|
17
17
|
|
|
18
18
|
New here? [docs/GETTING-STARTED.md](docs/GETTING-STARTED.md) walks through shadow mode, enforcing, MCP servers, your own agents and CI in about ten minutes.
|
|
19
19
|
|
|
@@ -35,7 +35,9 @@ What the gate decides (enforce mode, nothing executed):
|
|
|
35
35
|
| Kiro (IDE) | ✅ verified live |
|
|
36
36
|
| Cursor (CLI) | ✅ verified live: blocks before execution |
|
|
37
37
|
| GitHub Copilot CLI | ✅ verified live: blocks before execution, withholds injected tool results |
|
|
38
|
-
| Gemini CLI
|
|
38
|
+
| Gemini CLI | ✅ verified live: blocks before execution, withholds injected tool results |
|
|
39
|
+
| Windsurf (Devin.app) | ✅ verified live: blocks before execution, refuses to read injected files |
|
|
40
|
+
| Antigravity CLI, OpenCode | built from their documented hooks/plugins; live verification in progress |
|
|
39
41
|
| Your own agents (Claude Agent SDK, Strands, LangChain / LangGraph, OpenAI Agents SDK, Google ADK, CrewAI, Semantic Kernel) | ✅ in-process: `guard_options(options)`, `InterlockHooks()`, `guard_tools(tools)`, `guard_agent_tools(agent)`, `guard_agent(agent)`, `crewai.install()`, `guard_kernel(kernel)` |
|
|
40
42
|
| JavaScript / TypeScript agents (Vercel AI SDK, Claude Agent SDK for TS, Mastra) | ✅ `@interlockd/sdk`: `guardTools(tools)`, `guardOptions(options)` ([sdk/js](sdk/js)) |
|
|
41
43
|
| Any MCP client | ✅ the MCP gateway (`interlock gateway wrap`) gates every stdio and remote (HTTP) MCP server, hooks or not |
|
|
@@ -85,7 +87,7 @@ The full threat model is in [docs/THREAT-MODEL.md](docs/THREAT-MODEL.md). Latenc
|
|
|
85
87
|
|
|
86
88
|
```yaml
|
|
87
89
|
- uses: actions/checkout@v7
|
|
88
|
-
- uses: interlockd/interlock@v0.1.
|
|
90
|
+
- uses: interlockd/interlock@v0.1.4 # scan MCP/agent configs, test .interlock/policy.yaml
|
|
89
91
|
with: { fail-on: high }
|
|
90
92
|
- uses: github/codeql-action/upload-sarif@v3
|
|
91
93
|
if: always()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Gemini CLI hooks (measured)
|
|
2
|
+
|
|
3
|
+
**Status: verified live on Gemini CLI 0.65.0** (2026-10-24, headless `gemini -p --approval-mode yolo`, API-key auth,
|
|
4
|
+
`interlock capture --scratch`). Sources for the contract: https://geminicli.com/docs/hooks/ and /docs/hooks/reference/.
|
|
5
|
+
Redacted captures are in `daemon/tests/fixtures/gemini-cli/` and replayed by `test_live_captured_payloads`.
|
|
6
|
+
|
|
7
|
+
Note: Google replaced Gemini CLI with Antigravity CLI for unpaid-tier and Google One users on 2026-06-18; Gemini CLI
|
|
8
|
+
still runs with an API key. Antigravity's hook contract is separate (`adapters/antigravity/`).
|
|
9
|
+
|
|
10
|
+
## What Interlock registers
|
|
11
|
+
|
|
12
|
+
`~/.gemini/settings.json` (other settings and hooks preserved):
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{"hooks": {
|
|
16
|
+
"BeforeTool": [{"matcher": ".*", "hooks": [{"name": "interlock", "type": "command", "command": "sh ~/.interlock/adapters/gemini-cli/interlock-hook.sh", "timeout": 5000}]}],
|
|
17
|
+
"AfterTool": [{"matcher": ".*", "hooks": [{"name": "interlock", "type": "command", "command": "sh ~/.interlock/adapters/gemini-cli/interlock-hook.sh", "timeout": 5000}]}]}}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`timeout` is in milliseconds. The hooks fired without `/hooks enable`. Shell is `run_shell_command`; files are
|
|
21
|
+
`read_file`, `list_directory`, ...; MCP tools are `mcp_<server>_<tool>` (not exercised in the capture).
|
|
22
|
+
|
|
23
|
+
## Measured input
|
|
24
|
+
|
|
25
|
+
| Event | Keys |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `BeforeTool` | `session_id`, `transcript_path`, `cwd`, `hook_event_name`, `timestamp`, `tool_name`, `tool_input` |
|
|
28
|
+
| `AfterTool` | the above plus `tool_response {llmContent, returnDisplay}` |
|
|
29
|
+
|
|
30
|
+
Differences from the docs: `timestamp` is present (undocumented); `mcp_context` is absent for built-in tools; there is
|
|
31
|
+
**no tool-call id**, so `interlock capture report` pairs a refused call with a later result by session and
|
|
32
|
+
`tool_input`. Shell results arrive wrapped by Gemini itself: `llmContent` is `<untrusted_context>\nOutput: ...\n</untrusted_context>`.
|
|
33
|
+
The shell tool adds a `description` key to `tool_input`.
|
|
34
|
+
|
|
35
|
+
## Output, and what was observed
|
|
36
|
+
|
|
37
|
+
| Goal | Interlock emits | Observed |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Refuse a call | `{"decision": "deny", "reason": "..."}` | ✅ `git push --force origin main` not executed; Gemini printed "Tool execution blocked: [interlock] ..." and no AfterTool followed |
|
|
40
|
+
| Ask the user | — | no ask in the contract; Interlock degrades ask to deny with a notice |
|
|
41
|
+
| Withhold an injected result | `{"decision": "deny", "reason": "<notice>"}` on AfterTool | ✅ `read_file README.md`: "Tool result blocked: [interlock] ..."; the model saw only the notice |
|
|
42
|
+
| Warn after a result | `{"hookSpecificOutput": {"hookEventName": "AfterTool", "additionalContext": "..."}}` | not exercised live (unit-tested) |
|
|
43
|
+
| No opinion | no output, exit 0 | ✅ benign calls ran normally |
|
|
44
|
+
|
|
45
|
+
## Agent behaviour worth knowing
|
|
46
|
+
|
|
47
|
+
After the README was withheld, Gemini went looking for the reason: `env | grep -i INTERLOCK`, `which interlock`,
|
|
48
|
+
`list_directory ~/.interlock` (refused by Gemini's own workspace sandbox), then a python one-liner described as
|
|
49
|
+
"Inspect obfuscated text of README.md to understand content without triggering policy", which printed the README in
|
|
50
|
+
ROT13. That output was not flagged at the time. Two fixes followed:
|
|
51
|
+
|
|
52
|
+
- the local probe also scores ROT13 and base64 decodings of each tool result (`decoded_views` in `judges/local_probe.py`);
|
|
53
|
+
- `flow.reread_withheld`: once a file's contents are withheld, a shell (or other non-read) call naming that file in the
|
|
54
|
+
same session is asked about, which Gemini receives as a deny.
|
|
55
|
+
|
|
56
|
+
`post-shell-rot13-inject.json` is that capture; it is withheld now.
|
|
57
|
+
|
|
58
|
+
## Running it
|
|
59
|
+
|
|
60
|
+
- Headless mode needs a trusted folder: set `GEMINI_CLI_TRUST_WORKSPACE=true` (or `--skip-trust`); otherwise Gemini
|
|
61
|
+
exits before any tool runs and YOLO is overridden to default.
|
|
62
|
+
- Gemini CLI runs on Node; a Node whose CA store cannot verify `generativelanguage.googleapis.com` fails with
|
|
63
|
+
`TypeError: fetch failed sending request` and nine retries. That is the Node install, not the hooks.
|
|
64
|
+
- Free-tier keys: `gemini-2.5-flash` returned "no longer available to new users"; `gemini-3.8-flash` worked (20 requests
|
|
65
|
+
per day).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Windsurf (Cascade) hooks (measured)
|
|
2
|
+
|
|
3
|
+
**Status: verified live on Windsurf 3.10.67** (2026-10-24; the app now ships as **Devin.app**, bundle id
|
|
4
|
+
`com.exafunction.windsurf`; docs at https://docs.devin.ai/desktop/cascade/hooks). Captured with `interlock capture`
|
|
5
|
+
and project-level hooks in a scratch workspace; redacted payloads in `daemon/tests/fixtures/windsurf/`.
|
|
6
|
+
|
|
7
|
+
## What Interlock registers
|
|
8
|
+
|
|
9
|
+
`interlock init` adds to `~/.codeium/windsurf/hooks.json` (your own hooks are kept): `pre_run_command`,
|
|
10
|
+
`pre_mcp_tool_use`, `pre_read_code`, `pre_write_code`, each
|
|
11
|
+
`{"command": "sh ~/.interlock/adapters/windsurf/interlock-hook.sh pre", "show_output": true}`.
|
|
12
|
+
|
|
13
|
+
Windsurf also merges system (`/Library/Application Support/Devin/hooks.json`) and workspace (`.devin/hooks.json`,
|
|
14
|
+
legacy `.windsurf/hooks.json`) files. Measured: **both** workspace files load when both exist, although the docs say
|
|
15
|
+
the legacy one is skipped. Hooks don't run in Restricted Mode (trust the folder).
|
|
16
|
+
|
|
17
|
+
## Measured input
|
|
18
|
+
|
|
19
|
+
Every event: `agent_action_name`, `trajectory_id` (one per Cascade conversation), `execution_id` (one per Cascade
|
|
20
|
+
turn, shared by every call in it), `timestamp`, `tool_info`. No `model_name` arrived.
|
|
21
|
+
|
|
22
|
+
| Event | `tool_info` |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `pre_run_command` / `post_run_command` | `command_line`, `cwd` — **`cwd` is always `null`** |
|
|
25
|
+
| `pre_read_code` / `post_read_code` | `file_path` (absolute) — **no content** |
|
|
26
|
+
| `post_run_command` | same as pre — **no output** |
|
|
27
|
+
|
|
28
|
+
Hooks run with the workspace as their working directory. The shim sends that directory (`adapter.cwd`) and the daemon
|
|
29
|
+
uses it when `tool_info.cwd` is null; without it a repository policy in the workspace was never found and an
|
|
30
|
+
enforcing scratch policy did not apply.
|
|
31
|
+
|
|
32
|
+
**Every hook runs twice**, ~60 ms apart, with identical payloads. The daemon answers the repeat from the first
|
|
33
|
+
decision (keyed on `execution_id`, event and `tool_info`, 5 s), so it is recorded and counted once.
|
|
34
|
+
|
|
35
|
+
## Output, and what was observed
|
|
36
|
+
|
|
37
|
+
| Goal | Interlock emits | Observed |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Refuse a call | exit 2, reason on stderr | ✅ `git push --force origin main` not run; Cascade showed "Hook block" and quoted the reason |
|
|
40
|
+
| Refuse an injected file before it is read | exit 2 on `pre_read_code` (see below) | ✅ README.md refused; Cascade reported "the file was not read because it appears to contain instructions addressed to the agent" |
|
|
41
|
+
| Read a withheld file another way | exit 2 (`flow.reread_withheld`, ask → refusal) | ✅ `cat README.md` refused; Cascade asked the user to confirm |
|
|
42
|
+
| Withhold or annotate a result | — | not possible: post hooks can't change anything and carry no result |
|
|
43
|
+
| No opinion | exit 0 | ✅ `ls -la` ran |
|
|
44
|
+
|
|
45
|
+
## Reads are probed from disk
|
|
46
|
+
|
|
47
|
+
Because no hook carries file contents, the daemon reads the file named by `pre_read_code` itself (up to 256 KiB, regular
|
|
48
|
+
files only, never a sensitive path) and runs the injection probe on it. An injected file is refused before Cascade
|
|
49
|
+
reads it, and the path is remembered for `flow.reread_withheld`. Shell output can't be checked on Windsurf.
|
|
@@ -22,7 +22,7 @@ Prove the pipeline end-to-end before touching a real dataset. Each sample is han
|
|
|
22
22
|
importer's expected shape and imports with **0 skipped**:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
|
|
25
|
+
# from the repository root
|
|
26
26
|
.venv/bin/interlock corpus import --from agentdojo corpus/external/samples/agentdojo.sample.json --out corpus/external/imported/agentdojo.jsonl
|
|
27
27
|
.venv/bin/interlock corpus import --from injecagent corpus/external/samples/injecagent.sample.json --out corpus/external/imported/injecagent.jsonl
|
|
28
28
|
.venv/bin/interlock corpus import --from agentdrift corpus/external/samples/agentdrift.sample.json --out corpus/external/imported/agentdrift.jsonl
|
|
@@ -79,7 +79,7 @@ Always confirm the field mapping against a real sample before a full import (the
|
|
|
79
79
|
defensive but best-effort). Import writes **unlabelled** cases:
|
|
80
80
|
|
|
81
81
|
```sh
|
|
82
|
-
|
|
82
|
+
# from the repository root
|
|
83
83
|
.venv/bin/interlock corpus import --from agentdojo corpus/external/raw/agentdojo.json --out corpus/external/imported/agentdojo.jsonl
|
|
84
84
|
.venv/bin/interlock corpus import --from injecagent corpus/external/raw/injecagent.json --out corpus/external/imported/injecagent.jsonl
|
|
85
85
|
.venv/bin/interlock corpus import --from agentdrift corpus/external/raw/agentdrift.json --out corpus/external/imported/agentdrift.jsonl
|
|
@@ -9,7 +9,8 @@ pipx install interlockd # or: uv tool install interlockd, or: brew instal
|
|
|
9
9
|
interlock init
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
macOS and Linux. On Windows, do everything here inside WSL2 (Ubuntu) and run your agents from the WSL terminal.
|
|
12
|
+
macOS and Linux. On Windows, do everything here inside WSL2 (Ubuntu) and run your agents from the WSL terminal. (Natively on
|
|
13
|
+
Windows, the daemon and the SDKs already work: see the user guide, §2.)
|
|
13
14
|
|
|
14
15
|
`init` lists everything it will change and asks first. It then:
|
|
15
16
|
|
|
@@ -27,7 +27,7 @@ This guide covers what Interlock does today and how to run it. It grows with the
|
|
|
27
27
|
3. **Mandate.** An optional Cedar policy expresses what the agent is authorized to do (which remotes, which branches, which capabilities, how many destructive operations per session). Denials become findings.
|
|
28
28
|
4. **Composition.** The strongest action wins (`block` > `ask` > `model` > `flag` > `allow`). A denial budget escalates to the user after 3 consecutive or 20 total denials in a session.
|
|
29
29
|
5. **Judge.** Rules routed to `model` are decided by a decision model (Strands Decider 2B, open weights) answering "did the user authorize this action?" when a judge backend is configured; without one they are recorded as unjudged. `interlock judge install` sets it up on this machine in one step (a private environment in `~/.interlock/judge`, about 5 GB of weights, and `judge.primary` in your policy); the daemon then starts and supervises it, in the background, and `interlock judge status` / `interlock judge remove` do what they say.
|
|
30
|
-
6. **Post-action probe.** Tool results are checked for instructions addressed to the agent. The **local probe** (heuristics, plus an optional classifier such as PromptGuard 2) is on by default and needs no AWS or network. Amazon Bedrock Guardrails (`judge.probe.enabled`) and the judge's `injection_present` are optional extra detectors. All detectors compose deny-wins: the strongest signal sets `warn` or `withhold`.
|
|
30
|
+
6. **Post-action probe.** Tool results are checked for instructions addressed to the agent. The **local probe** (heuristics, plus an optional classifier such as PromptGuard 2) is on by default and needs no AWS or network; it also scores ROT13 and base64 decodings of each result, because an agent whose read was withheld may print the same file encoded. Amazon Bedrock Guardrails (`judge.probe.enabled`) and the judge's `injection_present` are optional extra detectors. All detectors compose deny-wins: the strongest signal sets `warn` or `withhold`.
|
|
31
31
|
|
|
32
32
|
### Shadow and enforce
|
|
33
33
|
|
|
@@ -35,7 +35,7 @@ A new install runs in **shadow** mode: every decision is computed and recorded,
|
|
|
35
35
|
|
|
36
36
|
## 2. Install and run
|
|
37
37
|
|
|
38
|
-
Prerequisites: macOS or Linux, Python 3.12 or newer, `curl`, and at least one supported agent (§4). On Windows, install and run it inside WSL2 (Ubuntu), with your agents run from the WSL terminal
|
|
38
|
+
Prerequisites: macOS or Linux, Python 3.12 or newer, `curl`, and at least one supported agent (§4). On Windows, install and run it inside WSL2 (Ubuntu), with your agents run from the WSL terminal. Natively on Windows, `interlock start` runs the daemon for the Python and JavaScript SDKs, with the same policy, audit log, `scan`, `report` and `test`; `init`, `install`, `gateway`, `sandbox`, `selftest`, `capture` and `judge` need macOS or Linux for now and say so. Windows-native agents (an IDE running on Windows itself) aren't covered yet. Linux: the background service is a systemd user service; `interlock sandbox` needs `bubblewrap`.
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
41
|
pipx install interlockd # or: uv tool install interlockd
|
|
@@ -281,6 +281,8 @@ flow:
|
|
|
281
281
|
|
|
282
282
|
**Memory poisoning.** Agent instruction files (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursorrules`, `.github/copilot-instructions.md`, `.cursor/rules/`, ...) and MCP memory servers are read back into every future session. Writes to them are recorded (`memory.instruction_file_write`); content that reads like instructions to the agent asks (`memory.injected_instructions`); and once a session has taken in untrusted content, writing them asks (`flow.memory_poisoning`), so an injected instruction can't quietly make itself permanent.
|
|
283
283
|
|
|
284
|
+
**Re-reading a withheld file.** Once a file's contents are withheld as injected, a shell (or other non-read) call that names the same file later in the session asks (`flow.reread_withheld`). Measured on Gemini CLI: with its README withheld, the agent printed it through a ROT13 one-liner "without triggering policy". A plain read tool is not affected; its result is probed again.
|
|
285
|
+
|
|
284
286
|
Sessions are the agent's own session id for hook traffic, and the launching agent's process for MCP gateway traffic, so taint never crosses between agents.
|
|
285
287
|
|
|
286
288
|
### Rule catalog
|
|
@@ -636,7 +638,8 @@ plugin into Interlock's own environment, e.g. `pipx inject interlockd <package>`
|
|
|
636
638
|
## 8. Known limitations
|
|
637
639
|
|
|
638
640
|
- The optional judge is off by default. When enabled, some of its thresholds are still being calibrated; expect an `ask` on some actions you plainly requested (answering the confirmation resolves it).
|
|
639
|
-
- Post-action withholding is verified on Claude Code and
|
|
641
|
+
- Post-action withholding is verified on Claude Code, Codex, Copilot CLI and Gemini CLI. Cursor can replace results only for MCP tools; other Cursor results get a warning.
|
|
642
|
+
- Windsurf's hooks carry no tool results: Interlock reads the file named by a read hook itself and refuses an injected one before Cascade reads it, but shell and MCP output can't be checked or withheld there. There is no ask, so an `ask` is a refusal.
|
|
640
643
|
- Kiro has no native ask and ignores PostToolUse output, so on Kiro an `ask` renders as a block with a notice, and post-action warnings are record-only (`interlock log`, review index).
|
|
641
644
|
- The Decider server binds loopback without auth and handles one request at a time. It is a dev-grade local dependency.
|
|
642
645
|
- `interlock log` shows rule ids and probabilities, not prose rationales. The explainer is planned.
|
|
@@ -18,10 +18,12 @@ OGR conformance (design §5.2), served at the bare /v1 prefix the spec uses:
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import hashlib
|
|
21
22
|
import json
|
|
22
23
|
import logging
|
|
23
24
|
import socketserver
|
|
24
25
|
import threading
|
|
26
|
+
import time
|
|
25
27
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
26
28
|
from typing import Any
|
|
27
29
|
from urllib.parse import parse_qs, urlparse
|
|
@@ -51,6 +53,20 @@ from .render import (
|
|
|
51
53
|
log = logging.getLogger("interlockd.api")
|
|
52
54
|
|
|
53
55
|
|
|
56
|
+
|
|
57
|
+
# Measured on Windsurf (Devin.app) 3.10.67: every hook runs twice, ~60 ms apart, with the same execution_id. The
|
|
58
|
+
# repeat gets the first answer instead of a second decision (no duplicate audit record, no double denial count).
|
|
59
|
+
REPEAT_TTL_S = 5.0
|
|
60
|
+
_REPEATS: dict[tuple[str, str, str], tuple[float, bytes]] = {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _windsurf_repeat_key(path: str, body: dict) -> tuple[str, str, str] | None:
|
|
64
|
+
p = body.get("payload") if path == "/interlock/v1/hooks/windsurf" else None
|
|
65
|
+
if not isinstance(p, dict) or not p.get("execution_id"):
|
|
66
|
+
return None
|
|
67
|
+
digest = hashlib.sha256(json.dumps(p.get("tool_info"), sort_keys=True, default=str).encode()).hexdigest()
|
|
68
|
+
return str(p["execution_id"]), str(p.get("agent_action_name")), digest
|
|
69
|
+
|
|
54
70
|
class _Server(ThreadingHTTPServer):
|
|
55
71
|
"""HTTPServer.server_bind resolves the host with socket.getfqdn(), a reverse-DNS lookup that can stall for tens
|
|
56
72
|
of seconds on machines with slow DNS (seen on CI runners). The daemon binds loopback; it never needs the name."""
|
|
@@ -61,8 +77,9 @@ class _Server(ThreadingHTTPServer):
|
|
|
61
77
|
self.server_name, self.server_port = str(host), port
|
|
62
78
|
|
|
63
79
|
|
|
64
|
-
class _UnixServer(socketserver.ThreadingMixIn, socketserver.
|
|
65
|
-
"""The same API on a Unix socket created with mode 0600: only the owning user can connect at all.
|
|
80
|
+
class _UnixServer(socketserver.ThreadingMixIn, getattr(socketserver, "UnixStreamServer", socketserver.TCPServer)):
|
|
81
|
+
"""The same API on a Unix socket created with mode 0600: only the owning user can connect at all.
|
|
82
|
+
(Windows Python has no UnixStreamServer: the base falls back so this module imports, and nothing creates one.)"""
|
|
66
83
|
daemon_threads = True
|
|
67
84
|
|
|
68
85
|
def server_bind(self):
|
|
@@ -85,6 +102,8 @@ class ApiServer:
|
|
|
85
102
|
handler = self._make_handler()
|
|
86
103
|
self.httpd = _Server((host, port), handler)
|
|
87
104
|
self.httpd.daemon_threads = True
|
|
105
|
+
if unix_socket and not hasattr(socketserver, "UnixStreamServer"):
|
|
106
|
+
raise OSError("Unix sockets are not available on this platform")
|
|
88
107
|
self.unix = _UnixServer(unix_socket, handler) if unix_socket else None
|
|
89
108
|
self._thread: threading.Thread | None = None
|
|
90
109
|
self._unix_thread: threading.Thread | None = None
|
|
@@ -254,8 +273,9 @@ class ApiServer:
|
|
|
254
273
|
elif u.path in ("/interlock/v1/hooks/copilot-cli", "/interlock/v1/hooks/windsurf", "/interlock/v1/hooks/antigravity"):
|
|
255
274
|
fn = {"copilot-cli": normalize_copilot, "windsurf": normalize_windsurf,
|
|
256
275
|
"antigravity": normalize_antigravity}[u.path.rsplit("/", 1)[-1]]
|
|
276
|
+
extra = {"hook_cwd": shim.get("cwd")} if fn is normalize_windsurf and isinstance(shim.get("cwd"), str) else {}
|
|
257
277
|
event = fn(body.get("payload") or {}, event=str(shim.get("event") or "pre"),
|
|
258
|
-
agent_id=server.daemon.settings.agent_id, adapter_version=str(shim.get("version", "0")))
|
|
278
|
+
agent_id=server.daemon.settings.agent_id, adapter_version=str(shim.get("version", "0")), **extra)
|
|
259
279
|
elif u.path == "/interlock/v1/hooks/gemini-cli":
|
|
260
280
|
event = normalize_gemini_cli(body.get("payload") or {}, agent_id=server.daemon.settings.agent_id,
|
|
261
281
|
adapter_version=str(shim.get("version", "0")))
|
|
@@ -272,6 +292,16 @@ class ApiServer:
|
|
|
272
292
|
except Exception as e: # noqa: BLE001 boundary: malformed hook payloads must never take the daemon down
|
|
273
293
|
self._send(400, {"error": f"could not normalize payload: {type(e).__name__}"})
|
|
274
294
|
return
|
|
295
|
+
repeat_key = _windsurf_repeat_key(u.path, body)
|
|
296
|
+
if repeat_key is not None:
|
|
297
|
+
hit = _REPEATS.get(repeat_key)
|
|
298
|
+
if hit is not None and time.monotonic() - hit[0] < REPEAT_TTL_S:
|
|
299
|
+
self.send_response(200) # the same hook call again: same answer, not a second decision
|
|
300
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
301
|
+
self.send_header("Content-Length", str(len(hit[1])))
|
|
302
|
+
self.end_headers()
|
|
303
|
+
self.wfile.write(hit[1])
|
|
304
|
+
return
|
|
275
305
|
try:
|
|
276
306
|
verdict = server.daemon.decide(event)
|
|
277
307
|
except Exception as e:
|
|
@@ -290,6 +320,10 @@ class ApiServer:
|
|
|
290
320
|
|
|
291
321
|
if fmt == "text":
|
|
292
322
|
data = render_text(verdict).encode()
|
|
323
|
+
if repeat_key is not None:
|
|
324
|
+
if len(_REPEATS) > 512:
|
|
325
|
+
_REPEATS.clear()
|
|
326
|
+
_REPEATS[repeat_key] = (time.monotonic(), data)
|
|
293
327
|
captured(data)
|
|
294
328
|
self.send_response(200)
|
|
295
329
|
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
@@ -10,10 +10,10 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
def load_or_create_token(path: Path) -> str:
|
|
12
12
|
if path.exists():
|
|
13
|
-
return path.read_text().strip()
|
|
13
|
+
return path.read_text(encoding="utf-8").strip()
|
|
14
14
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
15
15
|
token = secrets.token_urlsafe(32)
|
|
16
|
-
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
16
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600)
|
|
17
17
|
with os.fdopen(fd, "w") as f:
|
|
18
18
|
f.write(token + "\n")
|
|
19
19
|
return token
|
|
@@ -31,7 +31,7 @@ def rotate_token(path: Path) -> str:
|
|
|
31
31
|
token = secrets.token_urlsafe(32)
|
|
32
32
|
tmp = path.with_name(path.name + ".new")
|
|
33
33
|
tmp.unlink(missing_ok=True)
|
|
34
|
-
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
34
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600)
|
|
35
35
|
with os.fdopen(fd, "w") as f:
|
|
36
36
|
f.write(token + "\n")
|
|
37
37
|
os.replace(tmp, path)
|
|
@@ -42,9 +42,9 @@ def run(rounds: int = 3, http_calls: int = 300, shim_calls: int = 100) -> dict[s
|
|
|
42
42
|
from .testkit import default_corpus_dir, load_cases
|
|
43
43
|
|
|
44
44
|
out: dict[str, dict] = {}
|
|
45
|
-
home = Path(tempfile.mkdtemp(prefix="ilb", dir="/tmp"))
|
|
45
|
+
home = Path(tempfile.mkdtemp(prefix="ilb", dir="/tmp" if os.name != "nt" else None)) # short: socket path limit
|
|
46
46
|
try:
|
|
47
|
-
(home / "token").write_text("tok\n")
|
|
47
|
+
(home / "token").write_text("tok\n", encoding="utf-8")
|
|
48
48
|
s = Settings(home=home)
|
|
49
49
|
pol = Policy(); pol.mode = "enforce"; pol.trust.workspace_roots = [str(home)]
|
|
50
50
|
d = Daemon(s, spool=Spool(s.spool_dir, fsync=False), repo=MemoryRepository(), policy=pol)
|
|
@@ -67,10 +67,10 @@ def run(rounds: int = 3, http_calls: int = 300, shim_calls: int = 100) -> dict[s
|
|
|
67
67
|
t0 = time.perf_counter(); d.decide(ev); t.append((time.perf_counter() - t0) * 1000)
|
|
68
68
|
out["probe 4 KB"] = _pct(t)
|
|
69
69
|
|
|
70
|
-
sock = str(home / "interlockd.sock")
|
|
70
|
+
sock = str(home / "interlockd.sock") if s.unix_socket else None # no Unix sockets on Windows
|
|
71
71
|
api = ApiServer(d, "tok", "127.0.0.1", 0, unix_socket=sock); api.start_background()
|
|
72
72
|
host, port = api.address
|
|
73
|
-
(home / "daemon.json").write_text(json.dumps({"listen": f"{host}:{port}"}))
|
|
73
|
+
(home / "daemon.json").write_text(json.dumps({"listen": f"{host}:{port}"}), encoding="utf-8")
|
|
74
74
|
payload = {"session_id": "b", "cwd": str(home), "hook_event_name": "PreToolUse", "tool_name": "Bash",
|
|
75
75
|
"tool_input": {"command": "ls -la"}, "tool_use_id": "t"}
|
|
76
76
|
body = json.dumps({"adapter": {"name": "claude-code", "version": "bench"}, "payload": payload}).encode()
|
|
@@ -82,7 +82,7 @@ def run(rounds: int = 3, http_calls: int = 300, shim_calls: int = 100) -> dict[s
|
|
|
82
82
|
out["http"] = _pct(t)
|
|
83
83
|
|
|
84
84
|
if shutil.which("curl"):
|
|
85
|
-
shim = home / "shim.sh"; shim.write_text(CLAUDE_SHIM_SRC.read_text().replace("__HARNESS__", "claude-code"))
|
|
85
|
+
shim = home / "shim.sh"; shim.write_text(CLAUDE_SHIM_SRC.read_text(encoding="utf-8").replace("__HARNESS__", "claude-code"), encoding="utf-8")
|
|
86
86
|
for label, extra in (("shim (tcp)", {"INTERLOCK_URL": f"http://{host}:{port}"}), ("shim (unix)", {})):
|
|
87
87
|
env = {**{k: v for k, v in os.environ.items() if k != "INTERLOCK_URL"}, "INTERLOCK_HOME": str(home), **extra}
|
|
88
88
|
t = []
|