interlockd 0.1.2__tar.gz → 0.1.3__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.2 → interlockd-0.1.3}/.github/workflows/ci.yml +38 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/CHANGELOG.md +9 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/PKG-INFO +6 -5
- {interlockd-0.1.2 → interlockd-0.1.3}/README.md +5 -4
- interlockd-0.1.3/adapters/gemini-cli/PAYLOADS.md +65 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/GETTING-STARTED.md +2 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/USER-GUIDE.md +5 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/github-workflow.yml +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/__init__.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/api.py +5 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/auth.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/bench.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/capture.py +20 -8
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/cli.py +56 -47
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/config.py +3 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/control.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/corpus.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/describe.py +1 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/flow.py +38 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/mandate.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/memory.py +6 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/packs.py +21 -16
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/gateway.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/insights.py +12 -9
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/client.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judge_setup.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/local_probe.py +28 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/mcp_http.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/mcp_proxy.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/gemini_cli.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/ogr.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/onboard.py +17 -17
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/otel.py +2 -2
- interlockd-0.1.3/interlockd/paths.py +96 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/pipeline.py +4 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy/__init__.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy/workspace.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/scan.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/service.py +72 -13
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/claude-code/interlock-hook.sh +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/kiro/interlock-hook.mjs +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/kiro/interlock-hook.sh +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/spool.py +30 -9
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/testkit.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/transcript/kiro.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/pyproject.toml +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/package.json +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/src/index.js +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/conftest.py +24 -2
- interlockd-0.1.3/tests/fixtures/gemini-cli/post-read-benign.json +15 -0
- interlockd-0.1.3/tests/fixtures/gemini-cli/post-read-inject.json +15 -0
- interlockd-0.1.3/tests/fixtures/gemini-cli/post-shell-benign.json +16 -0
- interlockd-0.1.3/tests/fixtures/gemini-cli/post-shell-rot13-inject.json +16 -0
- interlockd-0.1.3/tests/fixtures/gemini-cli/pre-read-benign.json +11 -0
- interlockd-0.1.3/tests/fixtures/gemini-cli/pre-shell-benign.json +12 -0
- interlockd-0.1.3/tests/fixtures/gemini-cli/pre-shell-block.json +12 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_aibom.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_audit_chain.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_calibration.py +12 -12
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_capture.py +29 -5
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_claude_agent_sdk.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_claude_code.py +16 -13
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_control.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_cursor_gemini.py +23 -10
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_examples.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_gateway.py +19 -18
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_insights.py +7 -6
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_interlock_test.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_js_sdk.py +6 -6
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_judge.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_judge_setup.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_local_probe.py +34 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_managed_policy.py +2 -2
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_policy.py +1 -1
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_proxy.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_server.py +11 -9
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_more_adapters.py +11 -10
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_ogr.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_onboard.py +19 -17
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_opencode.py +5 -4
- interlockd-0.1.3/tests/test_paths_windows.py +64 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_repo_policy.py +5 -5
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_sandbox.py +8 -8
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_scan.py +14 -12
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_shim_sh.py +4 -4
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_spool.py +1 -1
- interlockd-0.1.3/tests/test_text_encoding.py +28 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_transcript.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_unix_socket.py +3 -3
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_version.py +8 -4
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_watch.py +10 -10
- interlockd-0.1.2/adapters/gemini-cli/PAYLOADS.md +0 -36
- interlockd-0.1.2/interlockd/paths.py +0 -63
- {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/agent.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/decision.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/.github/dependabot.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/.github/pull_request_template.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/.github/workflows/release.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/.gitignore +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/CODE_OF_CONDUCT.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/CONTRIBUTING.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/LICENSE +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/NOTICE +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/SECURITY.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/action.yml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/antigravity/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/claude-code/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/claude-code/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/codex/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/codex/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/copilot-cli/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/cursor/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/kiro/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/opencode/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/adapters/windsurf/PAYLOADS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/benchmarks/agentdojo_template.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/benchmarks/injecagent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/benchmarks/nl2bash.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/_corpus.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/_corpus.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/exfil_and_secrets.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/git_and_history.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/injection_persistence_supplychain.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_confirmation.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_exfil.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_injection.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_intent.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/slow_hijack.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/samples/agentdojo.sample.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/samples/agentdrift.sample.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/samples/injecagent.sample.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/ogr-seed-v0/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/corpus/ogr-seed-v0/seed-v0.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/demo/demo.sh +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/demo/demo.tape +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/ADAPTERS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/BENCHMARKS.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/PERFORMANCE.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/THREAT-MODEL.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/mandate-example.cedar +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/policy-example.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/reports/M3-judge-calibration.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/reports/ogr-seed-v0-results.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/docs/reports/ogr-seed-v0-results.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/claude_agent_sdk_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/crewai_crew.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/google_adk_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/langgraph_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/openai_agents.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/semantic_kernel_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/strands_agent.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/policies/ci.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/policies/mcp-locked-down.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/examples/policies/strict-team.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/__main__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/aibom.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/calibration.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/compose.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/_corpus.yaml +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/egress.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/fileops.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/git.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/packages.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/persistence.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/secrets.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/shell.jsonl +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/engine.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/mcp.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/oauth.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/shellparse.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/egress.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/ids.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/claude_agent_sdk.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/crewai.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/google_adk.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/langchain.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/openai_agents.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/semantic_kernel.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/strands.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/guardrails.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/projector.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/strands_decider.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/ledger.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/mcp_server.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/models.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/claude_code.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/cursor.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/kiro.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/more.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/plugins.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy/managed.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy_lint.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/render.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/sandbox.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/schema/verdict.schema.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/selftest.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/opencode/interlock.js +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/drainer.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/dynamodb.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/memory.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/tiers.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/transcript/__init__.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/transcript/claude_code.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/watch.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/packaging/homebrew/Formula/interlock.rb +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/packaging/homebrew/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/packaging/homebrew/make_formula.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/README.md +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/src/index.d.ts +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fake_mcp_server.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/post-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/post-read-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/pre-bash-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/pre-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/post-bash-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/post-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/pre-bash-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/pre-bash-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/copilot-cli/post-bash-inject.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/copilot-cli/pre-bash-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/copilot-cli/pre-bash-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/post-read-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/post-shell-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/pre-read-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/pre-shell-benign.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/pre-shell-block.json +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_api.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_bench.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_corpus.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_crewai.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_dynamodb.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_egress.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_flow.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_google_adk.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_homebrew_formula.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_langchain.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mandate.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_http.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_oauth.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_memory.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_normalize_kiro.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_openai_agents.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_otel.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_packs.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_pipeline.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_plugins.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_policy_lint.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_selftest.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_semantic_kernel.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_strands.py +0 -0
- {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_tiers.py +0 -0
|
@@ -9,6 +9,44 @@ permissions:
|
|
|
9
9
|
contents: read
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
|
+
windows:
|
|
13
|
+
# native Windows: daemon, policy, audit, scan, SDKs (agent hooks and the sandbox stay macOS/Linux; WSL2)
|
|
14
|
+
runs-on: windows-latest
|
|
15
|
+
defaults:
|
|
16
|
+
run:
|
|
17
|
+
shell: bash
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
- uses: astral-sh/setup-uv@v7
|
|
21
|
+
- name: Install
|
|
22
|
+
run: |
|
|
23
|
+
uv venv --python 3.12
|
|
24
|
+
uv pip install -e ".[dev]" --python .venv/Scripts/python.exe
|
|
25
|
+
- name: Unit tests
|
|
26
|
+
run: .venv/Scripts/python.exe -m pytest -q -p no:cacheprovider
|
|
27
|
+
- name: Rule corpus gate
|
|
28
|
+
run: .venv/Scripts/interlock.exe test
|
|
29
|
+
- name: Live daemon on Windows (start, decide, audit, stop)
|
|
30
|
+
run: |
|
|
31
|
+
export INTERLOCK_HOME="$RUNNER_TEMP/ih" IL=.venv/Scripts/interlock.exe
|
|
32
|
+
$IL enforce && $IL start && $IL status
|
|
33
|
+
.venv/Scripts/python.exe - <<'PY'
|
|
34
|
+
import os
|
|
35
|
+
from interlockd.integrations.client import InterlockClient
|
|
36
|
+
c = InterlockClient()
|
|
37
|
+
cases = [("Bash", {"command": "git push --force origin main"}, "block"),
|
|
38
|
+
("Write", {"file_path": os.path.expanduser(r"~\.CLAUDE\settings.json"), "content": "{}"}, "block"),
|
|
39
|
+
("Read", {"file_path": "README.md"}, "allow")]
|
|
40
|
+
for tool, args, want in cases:
|
|
41
|
+
v = c.decide("pre_action", tool, args, cwd=os.getcwd())
|
|
42
|
+
print(tool, args, "->", v and v["disposition"])
|
|
43
|
+
assert v and v["disposition"] == want, v
|
|
44
|
+
PY
|
|
45
|
+
$IL audit verify
|
|
46
|
+
$IL stop
|
|
47
|
+
sleep 2
|
|
48
|
+
if curl -sf http://127.0.0.1:7391/interlock/v1/health; then echo "daemon still running after stop"; exit 1; fi
|
|
49
|
+
|
|
12
50
|
test:
|
|
13
51
|
runs-on: ${{ matrix.os }}
|
|
14
52
|
strategy:
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
- **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.
|
|
6
|
+
- 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).
|
|
7
|
+
- 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.
|
|
8
|
+
- 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".
|
|
9
|
+
- `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.
|
|
10
|
+
- Text files are read and written as UTF-8 on every platform.
|
|
11
|
+
|
|
3
12
|
## 0.1.2 (2026-10-24)
|
|
4
13
|
|
|
5
14
|
- 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: interlockd
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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,8 @@ 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, Antigravity CLI, OpenCode | built from their documented hooks/plugins; live verification in progress |
|
|
83
84
|
| 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
85
|
| JavaScript / TypeScript agents (Vercel AI SDK, Claude Agent SDK for TS, Mastra) | ✅ `@interlockd/sdk`: `guardTools(tools)`, `guardOptions(options)` ([sdk/js](sdk/js)) |
|
|
85
86
|
| Any MCP client | ✅ the MCP gateway (`interlock gateway wrap`) gates every stdio and remote (HTTP) MCP server, hooks or not |
|
|
@@ -129,7 +130,7 @@ The full threat model is in [docs/THREAT-MODEL.md](docs/THREAT-MODEL.md). Latenc
|
|
|
129
130
|
|
|
130
131
|
```yaml
|
|
131
132
|
- uses: actions/checkout@v7
|
|
132
|
-
- uses: interlockd/interlock@v0.1.
|
|
133
|
+
- uses: interlockd/interlock@v0.1.3 # scan MCP/agent configs, test .interlock/policy.yaml
|
|
133
134
|
with: { fail-on: high }
|
|
134
135
|
- uses: github/codeql-action/upload-sarif@v3
|
|
135
136
|
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,8 @@ 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, Antigravity CLI, OpenCode | built from their documented hooks/plugins; live verification in progress |
|
|
39
40
|
| 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
41
|
| JavaScript / TypeScript agents (Vercel AI SDK, Claude Agent SDK for TS, Mastra) | ✅ `@interlockd/sdk`: `guardTools(tools)`, `guardOptions(options)` ([sdk/js](sdk/js)) |
|
|
41
42
|
| Any MCP client | ✅ the MCP gateway (`interlock gateway wrap`) gates every stdio and remote (HTTP) MCP server, hooks or not |
|
|
@@ -85,7 +86,7 @@ The full threat model is in [docs/THREAT-MODEL.md](docs/THREAT-MODEL.md). Latenc
|
|
|
85
86
|
|
|
86
87
|
```yaml
|
|
87
88
|
- uses: actions/checkout@v7
|
|
88
|
-
- uses: interlockd/interlock@v0.1.
|
|
89
|
+
- uses: interlockd/interlock@v0.1.3 # scan MCP/agent configs, test .interlock/policy.yaml
|
|
89
90
|
with: { fail-on: high }
|
|
90
91
|
- uses: github/codeql-action/upload-sarif@v3
|
|
91
92
|
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).
|
|
@@ -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,7 @@ 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.
|
|
640
642
|
- 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
643
|
- The Decider server binds loopback without auth and handles one request at a time. It is a dev-grade local dependency.
|
|
642
644
|
- `interlock log` shows rule ids and probabilities, not prose rationales. The explainer is planned.
|
|
@@ -61,8 +61,9 @@ class _Server(ThreadingHTTPServer):
|
|
|
61
61
|
self.server_name, self.server_port = str(host), port
|
|
62
62
|
|
|
63
63
|
|
|
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.
|
|
64
|
+
class _UnixServer(socketserver.ThreadingMixIn, getattr(socketserver, "UnixStreamServer", socketserver.TCPServer)):
|
|
65
|
+
"""The same API on a Unix socket created with mode 0600: only the owning user can connect at all.
|
|
66
|
+
(Windows Python has no UnixStreamServer: the base falls back so this module imports, and nothing creates one.)"""
|
|
66
67
|
daemon_threads = True
|
|
67
68
|
|
|
68
69
|
def server_bind(self):
|
|
@@ -85,6 +86,8 @@ class ApiServer:
|
|
|
85
86
|
handler = self._make_handler()
|
|
86
87
|
self.httpd = _Server((host, port), handler)
|
|
87
88
|
self.httpd.daemon_threads = True
|
|
89
|
+
if unix_socket and not hasattr(socketserver, "UnixStreamServer"):
|
|
90
|
+
raise OSError("Unix sockets are not available on this platform")
|
|
88
91
|
self.unix = _UnixServer(unix_socket, handler) if unix_socket else None
|
|
89
92
|
self._thread: threading.Thread | None = None
|
|
90
93
|
self._unix_thread: threading.Thread | None = None
|
|
@@ -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 = []
|
|
@@ -60,7 +60,7 @@ def stop(home: Path) -> bool:
|
|
|
60
60
|
|
|
61
61
|
def active(home: Path, harness: str) -> bool:
|
|
62
62
|
try:
|
|
63
|
-
st = json.loads((home / STATE).read_text())
|
|
63
|
+
st = json.loads((home / STATE).read_text(encoding="utf-8"))
|
|
64
64
|
except (OSError, ValueError):
|
|
65
65
|
return False
|
|
66
66
|
try:
|
|
@@ -100,10 +100,17 @@ def load(home: Path, harness: str | None = None) -> dict[str, list[dict]]:
|
|
|
100
100
|
out: dict[str, list[dict]] = {}
|
|
101
101
|
for d in sorted(root.iterdir()) if root.exists() else []:
|
|
102
102
|
if d.is_dir() and (harness is None or d.name == harness):
|
|
103
|
-
out[d.name] = [json.loads(f.read_text()) for f in sorted(d.glob("*.json"))]
|
|
103
|
+
out[d.name] = [json.loads(f.read_text(encoding="utf-8")) for f in sorted(d.glob("*.json"))]
|
|
104
104
|
return out
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
def _same_call(pre: dict, post: dict) -> bool:
|
|
108
|
+
p, q = pre.get("payload") or {}, post.get("payload") or {}
|
|
109
|
+
return (post["normalized"]["kind"] == "post_action" and post["normalized"]["tool"] == pre["normalized"]["tool"]
|
|
110
|
+
and isinstance(p, dict) and isinstance(q, dict) and p.get("session_id") == q.get("session_id")
|
|
111
|
+
and p.get("tool_input") == q.get("tool_input"))
|
|
112
|
+
|
|
113
|
+
|
|
107
114
|
def report(caps: dict[str, list[dict]]) -> str:
|
|
108
115
|
if not caps:
|
|
109
116
|
return "nothing captured yet: is capture on (`interlock capture start`), the daemon running, and the agent's hooks installed?"
|
|
@@ -118,10 +125,13 @@ def report(caps: dict[str, list[dict]]) -> str:
|
|
|
118
125
|
for name, keys in sorted(events.items()):
|
|
119
126
|
lines.append(f" {name:<22} keys: {', '.join(sorted(keys))[:150]}")
|
|
120
127
|
posts = {r["normalized"]["tool_call_id"] for r in recs if r["normalized"]["kind"] == "post_action" and r["normalized"]["tool_call_id"]}
|
|
121
|
-
for r in recs:
|
|
128
|
+
for i, r in enumerate(recs):
|
|
122
129
|
n, v = r["normalized"], r["verdict"]
|
|
123
130
|
if n["kind"] == "pre_action" and v["mode"] == "enforce" and v["disposition"] in ("block", "ask"):
|
|
124
|
-
|
|
131
|
+
if n["tool_call_id"]:
|
|
132
|
+
held = "no" if n["tool_call_id"] in posts else "yes"
|
|
133
|
+
else: # no tool id (Gemini CLI): a result for the same call in the same session means it ran
|
|
134
|
+
held = "no" if any(_same_call(r, later) for later in recs[i + 1:]) else "yes (no result for this call)"
|
|
125
135
|
lines.append(f" refused {n['tool']:<18} {', '.join(v['rules'])[:60]:<60} block held: {held}")
|
|
126
136
|
if n["kind"] == "post_action" and v["disposition"] in ("withhold", "warn"):
|
|
127
137
|
lines.append(f" {v['disposition']:<8} result of {n['tool']:<14} reply: {(r.get('reply') or '')[:90]!r}")
|
|
@@ -157,7 +167,7 @@ def export(caps: dict[str, list[dict]], dst: Path) -> list[Path]:
|
|
|
157
167
|
for i, r in enumerate(recs, 1):
|
|
158
168
|
n = r["normalized"]
|
|
159
169
|
f = d / f"{i:02d}-{n['kind'].split('_')[0]}-{re.sub(r'[^A-Za-z0-9_.-]', '_', n['tool'])[:40]}.json"
|
|
160
|
-
f.write_text(json.dumps(redact(r["payload"]), indent=2) + "\n")
|
|
170
|
+
f.write_text(json.dumps(redact(r["payload"]), indent=2) + "\n", encoding="utf-8")
|
|
161
171
|
out.append(f)
|
|
162
172
|
return out
|
|
163
173
|
|
|
@@ -167,17 +177,19 @@ def scratch(path: Path) -> Path:
|
|
|
167
177
|
import subprocess
|
|
168
178
|
|
|
169
179
|
path.mkdir(parents=True, exist_ok=True)
|
|
170
|
-
(path / "README.md").write_text(SCRATCH_README)
|
|
180
|
+
(path / "README.md").write_text(SCRATCH_README, encoding="utf-8")
|
|
171
181
|
(path / ".interlock").mkdir(exist_ok=True)
|
|
172
|
-
(path / ".interlock" / "policy.yaml").write_text("# verification only: enforce inside this folder\nmode: enforce\n")
|
|
182
|
+
(path / ".interlock" / "policy.yaml").write_text("# verification only: enforce inside this folder\nmode: enforce\n", encoding="utf-8")
|
|
173
183
|
if not (path / ".git").exists():
|
|
174
184
|
subprocess.run(["git", "init", "-q"], cwd=path, check=False)
|
|
175
185
|
subprocess.run(["git", "remote", "add", "origin", "https://github.com/example/scratch.git"], cwd=path, check=False)
|
|
186
|
+
# a block that fails must not reach a real host: pushes go to a path that does not exist
|
|
187
|
+
subprocess.run(["git", "config", "remote.origin.pushurl", str(path / ".git" / "no-such-remote")], cwd=path, check=False)
|
|
176
188
|
return path
|
|
177
189
|
|
|
178
190
|
|
|
179
191
|
def _write(path: Path, data: dict) -> None:
|
|
180
192
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
-
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
193
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_BINARY", 0), 0o600)
|
|
182
194
|
with os.fdopen(fd, "w") as f:
|
|
183
195
|
f.write(json.dumps(data, indent=2) + "\n")
|