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.
Files changed (262) hide show
  1. {interlockd-0.1.2 → interlockd-0.1.3}/.github/workflows/ci.yml +38 -0
  2. {interlockd-0.1.2 → interlockd-0.1.3}/CHANGELOG.md +9 -0
  3. {interlockd-0.1.2 → interlockd-0.1.3}/PKG-INFO +6 -5
  4. {interlockd-0.1.2 → interlockd-0.1.3}/README.md +5 -4
  5. interlockd-0.1.3/adapters/gemini-cli/PAYLOADS.md +65 -0
  6. {interlockd-0.1.2 → interlockd-0.1.3}/docs/GETTING-STARTED.md +2 -1
  7. {interlockd-0.1.2 → interlockd-0.1.3}/docs/USER-GUIDE.md +5 -3
  8. {interlockd-0.1.2 → interlockd-0.1.3}/examples/github-workflow.yml +1 -1
  9. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/__init__.py +1 -1
  10. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/api.py +5 -2
  11. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/auth.py +3 -3
  12. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/bench.py +5 -5
  13. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/capture.py +20 -8
  14. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/cli.py +56 -47
  15. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/config.py +3 -2
  16. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/control.py +2 -2
  17. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/corpus.py +2 -2
  18. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/describe.py +1 -0
  19. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/flow.py +38 -1
  20. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/mandate.py +1 -1
  21. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/memory.py +6 -2
  22. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/packs.py +21 -16
  23. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/gateway.py +5 -5
  24. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/insights.py +12 -9
  25. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/client.py +3 -3
  26. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judge_setup.py +4 -4
  27. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/local_probe.py +28 -0
  28. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/mcp_http.py +3 -3
  29. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/mcp_proxy.py +5 -5
  30. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/gemini_cli.py +2 -2
  31. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/ogr.py +2 -2
  32. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/onboard.py +17 -17
  33. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/otel.py +2 -2
  34. interlockd-0.1.3/interlockd/paths.py +96 -0
  35. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/pipeline.py +4 -0
  36. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy/__init__.py +1 -1
  37. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy/workspace.py +1 -1
  38. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/scan.py +5 -5
  39. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/service.py +72 -13
  40. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/claude-code/interlock-hook.sh +1 -1
  41. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/kiro/interlock-hook.mjs +1 -1
  42. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/kiro/interlock-hook.sh +1 -1
  43. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/spool.py +30 -9
  44. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/testkit.py +4 -4
  45. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/transcript/kiro.py +1 -1
  46. {interlockd-0.1.2 → interlockd-0.1.3}/pyproject.toml +1 -1
  47. {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/package.json +2 -2
  48. {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/src/index.js +1 -1
  49. {interlockd-0.1.2 → interlockd-0.1.3}/tests/conftest.py +24 -2
  50. interlockd-0.1.3/tests/fixtures/gemini-cli/post-read-benign.json +15 -0
  51. interlockd-0.1.3/tests/fixtures/gemini-cli/post-read-inject.json +15 -0
  52. interlockd-0.1.3/tests/fixtures/gemini-cli/post-shell-benign.json +16 -0
  53. interlockd-0.1.3/tests/fixtures/gemini-cli/post-shell-rot13-inject.json +16 -0
  54. interlockd-0.1.3/tests/fixtures/gemini-cli/pre-read-benign.json +11 -0
  55. interlockd-0.1.3/tests/fixtures/gemini-cli/pre-shell-benign.json +12 -0
  56. interlockd-0.1.3/tests/fixtures/gemini-cli/pre-shell-block.json +12 -0
  57. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_aibom.py +2 -2
  58. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_audit_chain.py +4 -4
  59. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_calibration.py +12 -12
  60. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_capture.py +29 -5
  61. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_claude_agent_sdk.py +1 -1
  62. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_claude_code.py +16 -13
  63. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_control.py +2 -2
  64. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_cursor_gemini.py +23 -10
  65. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_examples.py +2 -2
  66. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_gateway.py +19 -18
  67. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_insights.py +7 -6
  68. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_interlock_test.py +3 -3
  69. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_js_sdk.py +6 -6
  70. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_judge.py +1 -1
  71. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_judge_setup.py +3 -3
  72. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_local_probe.py +34 -1
  73. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_managed_policy.py +2 -2
  74. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_policy.py +1 -1
  75. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_proxy.py +4 -4
  76. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_server.py +11 -9
  77. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_more_adapters.py +11 -10
  78. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_ogr.py +3 -3
  79. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_onboard.py +19 -17
  80. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_opencode.py +5 -4
  81. interlockd-0.1.3/tests/test_paths_windows.py +64 -0
  82. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_repo_policy.py +5 -5
  83. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_sandbox.py +8 -8
  84. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_scan.py +14 -12
  85. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_shim_sh.py +4 -4
  86. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_spool.py +1 -1
  87. interlockd-0.1.3/tests/test_text_encoding.py +28 -0
  88. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_transcript.py +3 -3
  89. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_unix_socket.py +3 -3
  90. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_version.py +8 -4
  91. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_watch.py +10 -10
  92. interlockd-0.1.2/adapters/gemini-cli/PAYLOADS.md +0 -36
  93. interlockd-0.1.2/interlockd/paths.py +0 -63
  94. {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/agent.yml +0 -0
  95. {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  96. {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  97. {interlockd-0.1.2 → interlockd-0.1.3}/.github/ISSUE_TEMPLATE/decision.yml +0 -0
  98. {interlockd-0.1.2 → interlockd-0.1.3}/.github/dependabot.yml +0 -0
  99. {interlockd-0.1.2 → interlockd-0.1.3}/.github/pull_request_template.md +0 -0
  100. {interlockd-0.1.2 → interlockd-0.1.3}/.github/workflows/release.yml +0 -0
  101. {interlockd-0.1.2 → interlockd-0.1.3}/.gitignore +0 -0
  102. {interlockd-0.1.2 → interlockd-0.1.3}/CODE_OF_CONDUCT.md +0 -0
  103. {interlockd-0.1.2 → interlockd-0.1.3}/CONTRIBUTING.md +0 -0
  104. {interlockd-0.1.2 → interlockd-0.1.3}/LICENSE +0 -0
  105. {interlockd-0.1.2 → interlockd-0.1.3}/NOTICE +0 -0
  106. {interlockd-0.1.2 → interlockd-0.1.3}/SECURITY.md +0 -0
  107. {interlockd-0.1.2 → interlockd-0.1.3}/action.yml +0 -0
  108. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/README.md +0 -0
  109. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/antigravity/PAYLOADS.md +0 -0
  110. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/claude-code/PAYLOADS.md +0 -0
  111. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/claude-code/README.md +0 -0
  112. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/codex/PAYLOADS.md +0 -0
  113. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/codex/README.md +0 -0
  114. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/copilot-cli/PAYLOADS.md +0 -0
  115. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/cursor/PAYLOADS.md +0 -0
  116. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/kiro/PAYLOADS.md +0 -0
  117. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/opencode/PAYLOADS.md +0 -0
  118. {interlockd-0.1.2 → interlockd-0.1.3}/adapters/windsurf/PAYLOADS.md +0 -0
  119. {interlockd-0.1.2 → interlockd-0.1.3}/benchmarks/agentdojo_template.py +0 -0
  120. {interlockd-0.1.2 → interlockd-0.1.3}/benchmarks/injecagent.py +0 -0
  121. {interlockd-0.1.2 → interlockd-0.1.3}/benchmarks/nl2bash.py +0 -0
  122. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/README.md +0 -0
  123. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/_corpus.yaml +0 -0
  124. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/_corpus.yaml +0 -0
  125. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/exfil_and_secrets.jsonl +0 -0
  126. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/git_and_history.jsonl +0 -0
  127. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/injection_persistence_supplychain.jsonl +0 -0
  128. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_confirmation.jsonl +0 -0
  129. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_exfil.jsonl +0 -0
  130. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_injection.jsonl +0 -0
  131. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/judge_intent.jsonl +0 -0
  132. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/archetypes/slow_hijack.jsonl +0 -0
  133. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/README.md +0 -0
  134. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/samples/agentdojo.sample.json +0 -0
  135. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/samples/agentdrift.sample.json +0 -0
  136. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/external/samples/injecagent.sample.json +0 -0
  137. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/ogr-seed-v0/README.md +0 -0
  138. {interlockd-0.1.2 → interlockd-0.1.3}/corpus/ogr-seed-v0/seed-v0.jsonl +0 -0
  139. {interlockd-0.1.2 → interlockd-0.1.3}/demo/demo.sh +0 -0
  140. {interlockd-0.1.2 → interlockd-0.1.3}/demo/demo.tape +0 -0
  141. {interlockd-0.1.2 → interlockd-0.1.3}/docs/ADAPTERS.md +0 -0
  142. {interlockd-0.1.2 → interlockd-0.1.3}/docs/BENCHMARKS.md +0 -0
  143. {interlockd-0.1.2 → interlockd-0.1.3}/docs/PERFORMANCE.md +0 -0
  144. {interlockd-0.1.2 → interlockd-0.1.3}/docs/THREAT-MODEL.md +0 -0
  145. {interlockd-0.1.2 → interlockd-0.1.3}/docs/mandate-example.cedar +0 -0
  146. {interlockd-0.1.2 → interlockd-0.1.3}/docs/policy-example.yaml +0 -0
  147. {interlockd-0.1.2 → interlockd-0.1.3}/docs/reports/M3-judge-calibration.md +0 -0
  148. {interlockd-0.1.2 → interlockd-0.1.3}/docs/reports/ogr-seed-v0-results.jsonl +0 -0
  149. {interlockd-0.1.2 → interlockd-0.1.3}/docs/reports/ogr-seed-v0-results.md +0 -0
  150. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/README.md +0 -0
  151. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/claude_agent_sdk_agent.py +0 -0
  152. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/crewai_crew.py +0 -0
  153. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/google_adk_agent.py +0 -0
  154. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/langgraph_agent.py +0 -0
  155. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/openai_agents.py +0 -0
  156. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/semantic_kernel_agent.py +0 -0
  157. {interlockd-0.1.2 → interlockd-0.1.3}/examples/agents/strands_agent.py +0 -0
  158. {interlockd-0.1.2 → interlockd-0.1.3}/examples/policies/ci.yaml +0 -0
  159. {interlockd-0.1.2 → interlockd-0.1.3}/examples/policies/mcp-locked-down.yaml +0 -0
  160. {interlockd-0.1.2 → interlockd-0.1.3}/examples/policies/strict-team.yaml +0 -0
  161. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/__main__.py +0 -0
  162. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/aibom.py +0 -0
  163. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/calibration.py +0 -0
  164. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/compose.py +0 -0
  165. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/_corpus.yaml +0 -0
  166. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/egress.jsonl +0 -0
  167. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/fileops.jsonl +0 -0
  168. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/git.jsonl +0 -0
  169. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/packages.jsonl +0 -0
  170. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/persistence.jsonl +0 -0
  171. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/secrets.jsonl +0 -0
  172. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/data/packs/shell.jsonl +0 -0
  173. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/__init__.py +0 -0
  174. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/engine.py +0 -0
  175. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/mcp.py +0 -0
  176. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/oauth.py +0 -0
  177. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/det/shellparse.py +0 -0
  178. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/egress.py +0 -0
  179. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/ids.py +0 -0
  180. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/__init__.py +0 -0
  181. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/claude_agent_sdk.py +0 -0
  182. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/crewai.py +0 -0
  183. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/google_adk.py +0 -0
  184. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/langchain.py +0 -0
  185. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/openai_agents.py +0 -0
  186. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/semantic_kernel.py +0 -0
  187. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/integrations/strands.py +0 -0
  188. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/__init__.py +0 -0
  189. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/guardrails.py +0 -0
  190. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/projector.py +0 -0
  191. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/judges/strands_decider.py +0 -0
  192. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/ledger.py +0 -0
  193. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/mcp_server.py +0 -0
  194. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/models.py +0 -0
  195. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/__init__.py +0 -0
  196. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/claude_code.py +0 -0
  197. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/cursor.py +0 -0
  198. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/kiro.py +0 -0
  199. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/normalize/more.py +0 -0
  200. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/plugins.py +0 -0
  201. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy/managed.py +0 -0
  202. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/policy_lint.py +0 -0
  203. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/render.py +0 -0
  204. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/sandbox.py +0 -0
  205. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/schema/verdict.schema.json +0 -0
  206. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/selftest.py +0 -0
  207. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/shims/opencode/interlock.js +0 -0
  208. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/__init__.py +0 -0
  209. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/drainer.py +0 -0
  210. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/dynamodb.py +0 -0
  211. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/store/memory.py +0 -0
  212. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/tiers.py +0 -0
  213. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/transcript/__init__.py +0 -0
  214. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/transcript/claude_code.py +0 -0
  215. {interlockd-0.1.2 → interlockd-0.1.3}/interlockd/watch.py +0 -0
  216. {interlockd-0.1.2 → interlockd-0.1.3}/packaging/homebrew/Formula/interlock.rb +0 -0
  217. {interlockd-0.1.2 → interlockd-0.1.3}/packaging/homebrew/README.md +0 -0
  218. {interlockd-0.1.2 → interlockd-0.1.3}/packaging/homebrew/make_formula.py +0 -0
  219. {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/README.md +0 -0
  220. {interlockd-0.1.2 → interlockd-0.1.3}/sdk/js/src/index.d.ts +0 -0
  221. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fake_mcp_server.py +0 -0
  222. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/post-bash-inject.json +0 -0
  223. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/post-read-inject.json +0 -0
  224. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/pre-bash-block.json +0 -0
  225. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/claude-code/pre-bash-inject.json +0 -0
  226. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/post-bash-benign.json +0 -0
  227. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/post-bash-inject.json +0 -0
  228. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/pre-bash-benign.json +0 -0
  229. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/codex/pre-bash-block.json +0 -0
  230. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/copilot-cli/post-bash-inject.json +0 -0
  231. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/copilot-cli/pre-bash-benign.json +0 -0
  232. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/copilot-cli/pre-bash-block.json +0 -0
  233. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/post-read-benign.json +0 -0
  234. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/post-shell-benign.json +0 -0
  235. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/pre-read-benign.json +0 -0
  236. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/pre-shell-benign.json +0 -0
  237. {interlockd-0.1.2 → interlockd-0.1.3}/tests/fixtures/cursor/pre-shell-block.json +0 -0
  238. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_api.py +0 -0
  239. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_bench.py +0 -0
  240. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_corpus.py +0 -0
  241. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_crewai.py +0 -0
  242. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_dynamodb.py +0 -0
  243. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_egress.py +0 -0
  244. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_flow.py +0 -0
  245. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_google_adk.py +0 -0
  246. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_homebrew_formula.py +0 -0
  247. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_langchain.py +0 -0
  248. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mandate.py +0 -0
  249. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_http.py +0 -0
  250. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_mcp_oauth.py +0 -0
  251. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_memory.py +0 -0
  252. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_normalize_kiro.py +0 -0
  253. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_openai_agents.py +0 -0
  254. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_otel.py +0 -0
  255. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_packs.py +0 -0
  256. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_pipeline.py +0 -0
  257. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_plugins.py +0 -0
  258. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_policy_lint.py +0 -0
  259. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_selftest.py +0 -0
  260. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_semantic_kernel.py +0 -0
  261. {interlockd-0.1.2 → interlockd-0.1.3}/tests/test_strands.py +0 -0
  262. {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.2
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
- native Windows support is planned.
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, Windsurf, Antigravity CLI, OpenCode | built from their documented hooks/plugins; live verification in progress |
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.2 # scan MCP/agent configs, test .interlock/policy.yaml
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
- native Windows support is planned.
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, Windsurf, Antigravity CLI, OpenCode | built from their documented hooks/plugins; live verification in progress |
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.2 # scan MCP/agent configs, test .interlock/policy.yaml
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; 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`.
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 Codex. Cursor can replace results only for MCP tools; other Cursor results get a warning.
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.
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v7
12
- - uses: interlockd/interlock@v0.1.2
12
+ - uses: interlockd/interlock@v0.1.3
13
13
  with: { fail-on: high }
14
14
  - uses: github/codeql-action/upload-sarif@v3
15
15
  if: always()
@@ -1,3 +1,3 @@
1
1
  """Interlock daemon package."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
@@ -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.UnixStreamServer):
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")) # short path: the Unix socket has a length limit
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
- held = "no" if n["tool_call_id"] and n["tool_call_id"] in posts else ("yes" if n["tool_call_id"] else "unknown (no tool id)")
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")