two-key 0.7.0__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 (174) hide show
  1. two_key-0.7.0/AGENTS.md +316 -0
  2. two_key-0.7.0/CHANGES.md +3832 -0
  3. two_key-0.7.0/CONTRIBUTING.md +53 -0
  4. two_key-0.7.0/LICENSE +201 -0
  5. two_key-0.7.0/MANIFEST.in +16 -0
  6. two_key-0.7.0/PKG-INFO +783 -0
  7. two_key-0.7.0/README.md +743 -0
  8. two_key-0.7.0/ROADMAP.md +724 -0
  9. two_key-0.7.0/SECURITY.md +158 -0
  10. two_key-0.7.0/docs/COMPARISON.md +45 -0
  11. two_key-0.7.0/docs/CONNECTING.md +519 -0
  12. two_key-0.7.0/docs/FIT.md +94 -0
  13. two_key-0.7.0/docs/HOWTO.md +2568 -0
  14. two_key-0.7.0/docs/JUDGES_AND_QUORUM.md +199 -0
  15. two_key-0.7.0/docs/PYPI.md +197 -0
  16. two_key-0.7.0/docs/SCANNERS.md +796 -0
  17. two_key-0.7.0/docs/SCOPE.md +162 -0
  18. two_key-0.7.0/docs/SIEM.md +337 -0
  19. two_key-0.7.0/docs/THREAT_MODEL.md +791 -0
  20. two_key-0.7.0/docs/images/two-key-banner.png +0 -0
  21. two_key-0.7.0/examples/agents.yaml +55 -0
  22. two_key-0.7.0/examples/constitution.md +18 -0
  23. two_key-0.7.0/examples/custom_fields.yaml +76 -0
  24. two_key-0.7.0/examples/hard_rules.yaml +72 -0
  25. two_key-0.7.0/examples/judges.yaml +115 -0
  26. two_key-0.7.0/examples/scanning.yaml +51 -0
  27. two_key-0.7.0/examples/siem/elastic/README.md +118 -0
  28. two_key-0.7.0/examples/siem/elastic/dashboard.ndjson +6 -0
  29. two_key-0.7.0/examples/siem/elastic/docker-compose.yml +35 -0
  30. two_key-0.7.0/examples/siem/elastic/ingest-pipeline.json +39 -0
  31. two_key-0.7.0/examples/siem/elastic/make-certs.sh +45 -0
  32. two_key-0.7.0/examples/siem/elastic/receiver.py +328 -0
  33. two_key-0.7.0/examples/siem/elastic/siem.yaml +21 -0
  34. two_key-0.7.0/llms.txt +197 -0
  35. two_key-0.7.0/pyproject.toml +68 -0
  36. two_key-0.7.0/setup.cfg +4 -0
  37. two_key-0.7.0/tests/test_action_claim.py +83 -0
  38. two_key-0.7.0/tests/test_agent_access.py +144 -0
  39. two_key-0.7.0/tests/test_agent_decline.py +126 -0
  40. two_key-0.7.0/tests/test_agent_metadata.py +184 -0
  41. two_key-0.7.0/tests/test_agent_prompt.py +184 -0
  42. two_key-0.7.0/tests/test_args_size.py +95 -0
  43. two_key-0.7.0/tests/test_audit_digests.py +64 -0
  44. two_key-0.7.0/tests/test_b5_local_proxy.py +262 -0
  45. two_key-0.7.0/tests/test_ballot_identity.py +60 -0
  46. two_key-0.7.0/tests/test_caller_facts.py +266 -0
  47. two_key-0.7.0/tests/test_capability_key.py +162 -0
  48. two_key-0.7.0/tests/test_case_exact.py +148 -0
  49. two_key-0.7.0/tests/test_cli_tokens.py +233 -0
  50. two_key-0.7.0/tests/test_cli_witness.py +141 -0
  51. two_key-0.7.0/tests/test_cloud_model_any_judge.py +46 -0
  52. two_key-0.7.0/tests/test_config_strict.py +28 -0
  53. two_key-0.7.0/tests/test_credential_fingerprint.py +131 -0
  54. two_key-0.7.0/tests/test_custom_fields.py +746 -0
  55. two_key-0.7.0/tests/test_data_class_sets.py +126 -0
  56. two_key-0.7.0/tests/test_decision_record.py +80 -0
  57. two_key-0.7.0/tests/test_deep_args.py +79 -0
  58. two_key-0.7.0/tests/test_depth_boundary.py +226 -0
  59. two_key-0.7.0/tests/test_derive_robustness.py +118 -0
  60. two_key-0.7.0/tests/test_doc_diagrams.py +23 -0
  61. two_key-0.7.0/tests/test_doc_toc.py +99 -0
  62. two_key-0.7.0/tests/test_domain_allow.py +155 -0
  63. two_key-0.7.0/tests/test_duplicate_judge_ids.py +52 -0
  64. two_key-0.7.0/tests/test_fail_closed.py +138 -0
  65. two_key-0.7.0/tests/test_field_decisions.py +187 -0
  66. two_key-0.7.0/tests/test_fields_compat.py +74 -0
  67. two_key-0.7.0/tests/test_gateway_boundary.py +122 -0
  68. two_key-0.7.0/tests/test_gateway_denied.py +271 -0
  69. two_key-0.7.0/tests/test_gateway_locks.py +131 -0
  70. two_key-0.7.0/tests/test_guard.py +135 -0
  71. two_key-0.7.0/tests/test_in_process_agent.py +58 -0
  72. two_key-0.7.0/tests/test_issue_fixes.py +264 -0
  73. two_key-0.7.0/tests/test_judge_agent_separation.py +578 -0
  74. two_key-0.7.0/tests/test_judge_connection_reuse.py +97 -0
  75. two_key-0.7.0/tests/test_judge_hook_spec.py +35 -0
  76. two_key-0.7.0/tests/test_judge_locality.py +119 -0
  77. two_key-0.7.0/tests/test_key_backup.py +461 -0
  78. two_key-0.7.0/tests/test_ledger_error_cap.py +425 -0
  79. two_key-0.7.0/tests/test_ledger_key_file.py +83 -0
  80. two_key-0.7.0/tests/test_ledger_scale.py +98 -0
  81. two_key-0.7.0/tests/test_ledger_shared.py +87 -0
  82. two_key-0.7.0/tests/test_legacy_formats.py +78 -0
  83. two_key-0.7.0/tests/test_lone_judge.py +55 -0
  84. two_key-0.7.0/tests/test_maker_case.py +115 -0
  85. two_key-0.7.0/tests/test_mapping_inputs.py +190 -0
  86. two_key-0.7.0/tests/test_mcp_docs.py +54 -0
  87. two_key-0.7.0/tests/test_mcp_draft.py +190 -0
  88. two_key-0.7.0/tests/test_mcp_proxy.py +371 -0
  89. two_key-0.7.0/tests/test_mcp_proxy_sdk.py +270 -0
  90. two_key-0.7.0/tests/test_mcp_scan.py +252 -0
  91. two_key-0.7.0/tests/test_measure_once.py +92 -0
  92. two_key-0.7.0/tests/test_netloc_allowlist.py +51 -0
  93. two_key-0.7.0/tests/test_no_bidi_chars.py +36 -0
  94. two_key-0.7.0/tests/test_origin_argument.py +139 -0
  95. two_key-0.7.0/tests/test_payload_paths.py +95 -0
  96. two_key-0.7.0/tests/test_platform.py +199 -0
  97. two_key-0.7.0/tests/test_problem_f.py +582 -0
  98. two_key-0.7.0/tests/test_proposal_duplicate_keys.py +27 -0
  99. two_key-0.7.0/tests/test_proposal_text.py +112 -0
  100. two_key-0.7.0/tests/test_recover_and_anchor.py +247 -0
  101. two_key-0.7.0/tests/test_required_yes_default.py +51 -0
  102. two_key-0.7.0/tests/test_same_agent_rule.py +569 -0
  103. two_key-0.7.0/tests/test_save_envelope.py +58 -0
  104. two_key-0.7.0/tests/test_scan.py +734 -0
  105. two_key-0.7.0/tests/test_scan_encrypted.py +244 -0
  106. two_key-0.7.0/tests/test_scanners.py +476 -0
  107. two_key-0.7.0/tests/test_siem.py +660 -0
  108. two_key-0.7.0/tests/test_siem_export.py +283 -0
  109. two_key-0.7.0/tests/test_siem_sinks.py +529 -0
  110. two_key-0.7.0/tests/test_strict_parsing.py +144 -0
  111. two_key-0.7.0/tests/test_tenant_empty.py +23 -0
  112. two_key-0.7.0/tests/test_tenant_optin.py +137 -0
  113. two_key-0.7.0/tests/test_text_io.py +77 -0
  114. two_key-0.7.0/tests/test_tmp_hygiene.py +51 -0
  115. two_key-0.7.0/tests/test_token_lifetime.py +123 -0
  116. two_key-0.7.0/tests/test_two_key.py +455 -0
  117. two_key-0.7.0/tests/test_upgrade_notes.py +57 -0
  118. two_key-0.7.0/tests/test_upstream_fold.py +24 -0
  119. two_key-0.7.0/tests/test_witness_pin.py +442 -0
  120. two_key-0.7.0/tests/test_writable_paths.py +271 -0
  121. two_key-0.7.0/two_key/__init__.py +13 -0
  122. two_key-0.7.0/two_key/__main__.py +3 -0
  123. two_key-0.7.0/two_key/_platform.py +597 -0
  124. two_key-0.7.0/two_key/action.py +199 -0
  125. two_key-0.7.0/two_key/agent_access.py +96 -0
  126. two_key-0.7.0/two_key/agent_meta.py +139 -0
  127. two_key-0.7.0/two_key/agents.py +455 -0
  128. two_key-0.7.0/two_key/audit.py +90 -0
  129. two_key-0.7.0/two_key/backup.py +783 -0
  130. two_key-0.7.0/two_key/canonical.py +181 -0
  131. two_key-0.7.0/two_key/capability.py +222 -0
  132. two_key-0.7.0/two_key/cli.py +719 -0
  133. two_key-0.7.0/two_key/compiler.py +55 -0
  134. two_key-0.7.0/two_key/constitution.py +547 -0
  135. two_key-0.7.0/two_key/core.py +769 -0
  136. two_key-0.7.0/two_key/derive.py +593 -0
  137. two_key-0.7.0/two_key/fields.py +577 -0
  138. two_key-0.7.0/two_key/gateway.py +448 -0
  139. two_key-0.7.0/two_key/guard.py +205 -0
  140. two_key-0.7.0/two_key/identity.py +1106 -0
  141. two_key-0.7.0/two_key/judges/__init__.py +14 -0
  142. two_key-0.7.0/two_key/judges/anthropic.py +80 -0
  143. two_key-0.7.0/two_key/judges/base.py +103 -0
  144. two_key-0.7.0/two_key/judges/config.py +240 -0
  145. two_key-0.7.0/two_key/judges/credentials.py +154 -0
  146. two_key-0.7.0/two_key/judges/gemini.py +56 -0
  147. two_key-0.7.0/two_key/judges/llm.py +368 -0
  148. two_key-0.7.0/two_key/judges/ollama.py +56 -0
  149. two_key-0.7.0/two_key/judges/openai_compat.py +101 -0
  150. two_key-0.7.0/two_key/judges/transport.py +152 -0
  151. two_key-0.7.0/two_key/keys.py +140 -0
  152. two_key-0.7.0/two_key/ledger.py +1268 -0
  153. two_key-0.7.0/two_key/mcp_proxy/__init__.py +10 -0
  154. two_key-0.7.0/two_key/mcp_proxy/config.py +408 -0
  155. two_key-0.7.0/two_key/mcp_proxy/core.py +568 -0
  156. two_key-0.7.0/two_key/mcp_proxy/draft.py +301 -0
  157. two_key-0.7.0/two_key/mcp_proxy/sdk.py +226 -0
  158. two_key-0.7.0/two_key/netloc.py +96 -0
  159. two_key-0.7.0/two_key/policy_vm.py +342 -0
  160. two_key-0.7.0/two_key/quorum.py +515 -0
  161. two_key-0.7.0/two_key/scan.py +795 -0
  162. two_key-0.7.0/two_key/scanners.py +557 -0
  163. two_key-0.7.0/two_key/siem.py +1001 -0
  164. two_key-0.7.0/two_key/siem_export.py +266 -0
  165. two_key-0.7.0/two_key/siem_sinks.py +497 -0
  166. two_key-0.7.0/two_key/strict.py +141 -0
  167. two_key-0.7.0/two_key/testing.py +72 -0
  168. two_key-0.7.0/two_key/writable.py +200 -0
  169. two_key-0.7.0/two_key.egg-info/PKG-INFO +783 -0
  170. two_key-0.7.0/two_key.egg-info/SOURCES.txt +172 -0
  171. two_key-0.7.0/two_key.egg-info/dependency_links.txt +1 -0
  172. two_key-0.7.0/two_key.egg-info/entry_points.txt +2 -0
  173. two_key-0.7.0/two_key.egg-info/requires.txt +8 -0
  174. two_key-0.7.0/two_key.egg-info/top_level.txt +1 -0
@@ -0,0 +1,316 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for coding agents working in this clone of
4
+ [Insomniac-VibeLabs/two-key](https://github.com/Insomniac-VibeLabs/two-key).
5
+
6
+ This file is a short contributor guide for agents. Product and API context for
7
+ any LLM reader is in [llms.txt](llms.txt). Do not replace one with the other.
8
+
9
+ ## Contents
10
+
11
+ <!-- toc -->
12
+ - [Scope](#scope)
13
+ - [Branch and commit rules](#branch-and-commit-rules)
14
+ - [Install and version matrix](#install-and-version-matrix)
15
+ - [Tests and docs](#tests-and-docs)
16
+ - [Crypto and security changes](#crypto-and-security-changes)
17
+ - [Fail-closed defaults to preserve](#fail-closed-defaults-to-preserve)
18
+ <!-- /toc -->
19
+
20
+ ## Scope
21
+
22
+ Core only: signed constitution, Path A bytecode, Path B judge hooks,
23
+ monitored-agent hooks, encrypted ledger, single-use capability token, a
24
+ verify-only gateway, and the SIEM export. Beta (pre-1.0), maintainer-reviewed, not independently
25
+ audited. Apache-2.0.
26
+
27
+ Do **not** add a scanning or antivirus engine, a SIEM or log storage, chain
28
+ anchoring, or seed phrases here. The items in [ROADMAP.md](ROADMAP.md) are in
29
+ scope: key backup (shipped in 0.3.0), an MCP adapter (0.5.0), DLP and
30
+ antivirus hooks that call external scanners (0.6.0), ledger export for a SIEM
31
+ (0.7.0), and, after 1.0, PKI with
32
+ certificate recovery, hybrid ML-DSA signatures, a FIPS approved mode, and a
33
+ local GUI. Build them in the order and with the limits stated
34
+ there, unless the operator says otherwise. See [docs/SCOPE.md](docs/SCOPE.md).
35
+
36
+ ## Branch and commit rules
37
+
38
+ - Package 0.7.0 (package name `two-key`) is tag `v0.7.0` on `main`; 0.6.0 is
39
+ tag `v0.6.0`, 0.5.0 is
40
+ tag `v0.5.0`, and 0.4.0 (tag `v0.4.0`) was the first release from this repository. Tags `v0.3.1` and earlier are in the archived `two-key-concept` repository. There, tag `v0.3.0` stays on `e44be56`.
41
+ Tag `v0.2.4` stays on `fc836bb`.
42
+ Tag `v0.2.3` stays on `4a4352e`. Do not move `v0.7.0`, `v0.6.0`, `v0.5.0`, `v0.4.0`, `v0.3.1`, `v0.3.0`, or `v0.2.0`
43
+ (`2f756ac`). `v0.1.12` and `v0.1.6` stay on their trees. `working` is
44
+ where the next changes land.
45
+ - Do not push, force-push, or open PRs unless the operator asks.
46
+ - Never commit keys, ledgers, or credentials.
47
+ - Update `CHANGES.md` with every commit. Read
48
+ `CHANGES.md` "Upgrading from 0.1.12" before changing 0.2.0 config shapes.
49
+
50
+ ## Install and version matrix
51
+
52
+ Requires Python ≥ 3.10. Package name `two-key`, import name `two_key`,
53
+ version `0.7.0` on PyPI (`pip install "two-key[yaml]==0.7.0"`). Tag `v0.7.0` on `main` is not moved. docs/PYPI.md is the release process. This packaging does not change the version.
54
+ Linux, macOS, and Windows (from 0.6.0). CI runs tests, the demo,
55
+ the build, bandit, and CodeQL on all three (CONTRIBUTING).
56
+
57
+ ```bash
58
+ python3 -m venv .venv
59
+ . .venv/bin/activate
60
+ pip install -e ".[yaml]"
61
+ python -m unittest discover -s tests
62
+ python -m two_key demo
63
+ ```
64
+
65
+ CI does not use that floating install. `.github/workflows/tests.yml` pins
66
+ `uv==0.13.0` and runs `uv sync --locked --no-build --extra yaml --extra mcp`, which
67
+ installs `uv.lock` (cryptography, PyYAML, the MCP SDK, and their dependencies) as wheels.
68
+ Regenerate `uv.lock` with that uv when a dependency range changes, and
69
+ commit the lock. Do not treat the lock as a release pin for consumers.
70
+ Build, coverage, and bandit are pinned in their workflows, not in
71
+ `pyproject.toml`.
72
+
73
+ | Extra | Provides | When needed |
74
+ | --- | --- | --- |
75
+ | (none) | `cryptography>=41` | Ed25519, SHA-256, AES-256-GCM ledger |
76
+ | `yaml` | PyYAML | `.yaml` rules and judge configs |
77
+ | `mcp` | MCP Python SDK (`mcp>=2.2,<3`), PyYAML | `two-key mcp-proxy`, `two-key mcp-draft-specs` (0.5.0) |
78
+
79
+ Index install: `pip install "two-key[yaml]==0.7.0"`.
80
+ The unmoved tag is still `pip install "two-key[yaml] @ git+https://github.com/Insomniac-VibeLabs/two-key.git@v0.7.0"`.
81
+
82
+ ## Tests and docs
83
+
84
+ - Tests: `python -m unittest discover -s tests` (no network). Keep the suite
85
+ network-free; use `two_key.testing` doubles only where the docs allow.
86
+ - When a claim changes, update `README.md`, `ROADMAP.md`, `docs/HOWTO.md`,
87
+ `docs/CONNECTING.md` (a new connection or hook gets a row there),
88
+ `docs/FIT.md`, `docs/COMPARISON.md`, `docs/THREAT_MODEL.md`,
89
+ `docs/SCOPE.md`, `docs/SIEM.md`, `docs/PYPI.md`, `CHANGES.md`, `SECURITY.md`,
90
+ `CONTRIBUTING.md`, `AGENTS.md`, and `llms.txt` as needed.
91
+ - When a roadmap item ships, update `docs/SCOPE.md` and `docs/THREAT_MODEL.md`
92
+ in the same release.
93
+ - Keep [docs/HOWTO.md](docs/HOWTO.md) "Known trade-offs by configuration"
94
+ accurate when a config trade-off changes.
95
+ - Tables of contents are generated: after changing a heading, run
96
+ `python tests/test_doc_toc.py --write` (`tests/test_doc_toc.py` fails
97
+ while one is stale).
98
+
99
+ ## Crypto and security changes
100
+
101
+ Crypto here is Ed25519 and SHA-256 via `cryptography`, plus AES-256-GCM for
102
+ ledger ciphertext. The ledger key, witness key, and capability private key
103
+ live outside the ledger directory. The gateway must verify only; it must not
104
+ mint.
105
+
106
+ Before changing authorization, tokens, the gateway, the ledger, or judge ≠
107
+ agent identity checks: get a security review from the maintainer. Report
108
+ security findings privately, per [SECURITY.md](SECURITY.md).
109
+
110
+ ## Fail-closed defaults to preserve
111
+
112
+ - A missing ballot is not a yes.
113
+ - Both paths run once the call is well-formed and within limits. The one
114
+ exception (maintainer's decision, 0.5.0): a call that cannot be read under
115
+ its spec (`derive_failed:`) is not sent to the judges unless
116
+ `tool_args_on_derive_deny` is set, and Path B is ledgered as
117
+ `skipped:derive_failed`. Mismatches and rule denies still go to the
118
+ judges. Do not widen the skip.
119
+ - `authorize_from_agent` does not execute a tool.
120
+ - A decline (`{"decline": ...}`) is not a call: `agent_declined` deny,
121
+ neither path runs, never a token. A decline that also carries a tool,
122
+ arguments, or a claim is `malformed_proposal` (`tests/test_agent_decline.py`).
123
+ - `require_path_a_first` is not a skip.
124
+ - JSON/YAML inputs refuse duplicate keys and YAML merge keys. Oversized or malformed calls deny
125
+ before either path.
126
+ - Default quorum needs one judge and has no diversity floors.
127
+ When `required_yes` is unset, it resolves to
128
+ `min(2, number_of_judges)` (at least one); this is **not** a majority
129
+ rule. `min_responding` defaults to the effective `required_yes`
130
+ threshold. With `required_yes: 1`, two or more judges, and
131
+ `min_responding` 1, `TwoKey` warns at start (`lone_judge_warning`); it
132
+ does not refuse. `QuorumPolicy.high_assurance` / `profile: high_assurance`
133
+ adds requirements for judges from two makers (`min_makers`), one
134
+ local judge, and a local yes. Note that
135
+ `QuorumPolicy.without_diversity_floors()` explicitly defaults
136
+ `required_yes` to `2`.
137
+ - `tool_specs` is required. A disagreeing claim is a deny.
138
+ - An omitted `data_class` is `classified`, before the floor. Do not
139
+ default it to the tool's floor. Classes combine as a set
140
+ (`join_data_class`): `financial` and `medical` replace `personal`,
141
+ together they are `financial+medical`, and only `classified` (claimed,
142
+ floored, or omitted) is `classified` (`tests/test_data_class_sets.py`).
143
+ A `data_class_in` rule matches any class the call carries. Do not map display names to
144
+ counterparty ids or fuzzy-match them. The one wider form is a signed
145
+ `@domain` allow entry (v3): a plain address at exactly that ASCII domain,
146
+ no subdomains, no wildcards, never in `deny_counterparties`
147
+ (`tests/test_domain_allow.py`). Do not widen it.
148
+ - `agent_vocabulary()` is opt-in and lists syntax, never a value: no
149
+ `allow` list, enum value, integer bound, rule, threshold,
150
+ `deny_counterparties`, description, default, or `irreversible` flag
151
+ (`tests/test_agent_prompt.py`). Do not add a mode that sends them.
152
+ Without `vocabulary=`, the agent user prompt stays byte-for-byte the 0.3.0
153
+ prompt.
154
+ - Path A evaluates five built-in structured fields (`tool`, `amount_usd`,
155
+ `data_class`, `counterparty`, `irreversible`) and the operator's custom
156
+ fields (`two_key/fields.py`). Custom field values come from argument
157
+ paths in the signed spec only, never from the agent's claim or
158
+ `Action.raw`, or, for a field declared `source: caller`, from the
159
+ integration's `facts=` only: written in the arguments it denies
160
+ (`caller_field_in_arguments`), and the gateway takes the facts again
161
+ (`fact_mismatch`) (`tests/test_caller_facts.py`). The agent writes the
162
+ arguments, so an argument field is what the call says, not a verified
163
+ fact, and a caller fact is as true as its hook; do not describe fields as
164
+ verification
165
+ (HOWTO "What Two-Key verifies"); `LOAD` refuses any name but a built-in field, and custom
166
+ fields load with `LOAD_FIELD`. A mapped field is required. Every tool
167
+ maps each field a `deny_if_fields` rule reads or marks it
168
+ `not_applicable` (then it gets the signed default); a forgotten decision
169
+ is refused at signing and on load, never read as the default
170
+ (`tests/test_field_decisions.py`). A description is never compiled, sent to a
171
+ judge, or used in a reason. A `case: exact` ASCII string is compared and passed to the tool as
172
+ written; never forward an original value after a case-folded check
173
+ (`tests/test_case_exact.py`). A constitution with no custom fields must
174
+ stay byte-for-byte v1 (`tests/test_fields_compat.py`); one without a
175
+ 0.5.0 feature (a caller field, a `case: exact` field, a `not_applicable`
176
+ mark, an `@domain` allow entry) must not sign as v3. Each signs only as
177
+ v3, and a v1 or v2 envelope that carries one is refused.
178
+ - Format identifiers are `two-key-...` (0.4.0). Files with the old
179
+ `two-key-concept-...` formats are refused with a clear message, never
180
+ read (`tests/test_legacy_formats.py`). Do not change a format name
181
+ again without a new version suffix and a migration note. Path A evaluates
182
+ structured data, not English-language policy prose.
183
+ - No judge may be the monitored agent: a judge holding the agent's
184
+ credential (the same API token, or the same username and password) is
185
+ refused at any address, whether or not either side sends it; so are two
186
+ sides that send no credential on one address. Other overlaps warn
187
+ and are recorded. Addresses are compared as configured, without DNS.
188
+ - The ledger's witness public key is pinned: a head or `witness.pem` that
189
+ does not match the pin in the ledger, or the configured
190
+ `witness_public_key`, is refused. It changes only by `rotate_witness`.
191
+ - The gateway verifies only; it never mints.
192
+ - The guard (`two_key/guard.py`) uses a fixed claim and proposal per tool,
193
+ never the agent's; looks facts up before `authorize` and again before
194
+ `invoke`, under one lock; and treats a hook that raises or returns None as
195
+ a missing fact (deny). It does not import the MCP SDK
196
+ (`tests/test_guard.py`).
197
+ - The MCP proxy (`two_key/mcp_proxy/`): only `sdk.py` imports the MCP SDK;
198
+ `config.py`, `core.py`, and `draft.py` must not. Every `tools/call` goes
199
+ through the guard (`authorize`, then `gateway.invoke` in the same
200
+ process); the upstream call is the gateway's registered tool, never
201
+ called directly. Only signed tools are shown, with caller-supplied paths
202
+ removed from `inputSchema`; any other name is `tool_not_offered`,
203
+ ledgered. `mcp_proxy_started` is ledgered before serving. A
204
+ schema mismatch refuses to start unless `on_schema_change: warn`.
205
+ `unresolved_identity` refuses unless `allow_unresolved_identity: true`;
206
+ the library keeps warning. The claim comes from `proxy.yaml` only, never
207
+ from the agent. A deny returns a reason code, never a value. Nothing but
208
+ MCP on stdout. Drafts must not sign until the principal decides; server
209
+ text stays in ASCII comments. An input Path A reads that the upstream
210
+ schema leaves undescribed (no `description`, no `enum`) is a WARNING in
211
+ the draft, at `--constitution`, and at proxy start, for any field, never
212
+ a looser match (`core.undescribed_inputs`). SDK tests skip without the extra; CI installs
213
+ it (`--extra mcp`).
214
+ - Platforms (0.6.0): every key read or write, owner-only directory, lock,
215
+ and no-follow open goes through `two_key/_platform.py`. On POSIX it does
216
+ what the code did before (mode 0600/0700, `O_NOFOLLOW`, `flock`, the same
217
+ messages). On Windows a key is created with an access list for the
218
+ current user only, and one whose owner or allow entries name anyone but
219
+ the user, SYSTEM, Administrators, or OWNER RIGHTS is refused; a reparse
220
+ point is refused; the lock is `LockFileEx`. Do not accept a wider access
221
+ list or fall back to "no lock". Text files Two-Key writes use
222
+ `newline="\n"` and a named encoding (`tests/test_text_io.py`).
223
+ - Writable paths (0.6.0, `two_key/writable.py`): at start, `TwoKey` names
224
+ each file or folder of the running Python (program, import path, `.pth`
225
+ files, loaded modules and their compiled files) and of `check_paths`,
226
+ and every folder above them, that an account other than its own (or
227
+ root, SYSTEM, Administrators, TrustedInstaller) could change, through
228
+ `_platform.write_problem`. `warn` is the default; `on_writable_paths:
229
+ refuse` refuses; `high_assurance` always refuses and refuses `warn`
230
+ (maintainer's decision, 2026-10-16). A path that cannot be checked is
231
+ named, not passed. There is no setting that turns it off, and on POSIX a
232
+ group write bit counts for every group but 0; do not add either. It runs
233
+ before anything is written to the ledger (`tests/test_writable_paths.py`).
234
+ - Agent account (0.7.0, `two_key/agent_access.py`): the MCP proxy reads the
235
+ real user of its parent process (`_platform.parent_account`, never an
236
+ environment variable) before it opens the ledger, and refuses to start
237
+ when that account is Two-Key's own (`agent_same_account`), root
238
+ (`agent_account_root`), unknown (`agent_account_unknown`, Windows
239
+ included for now), or can change a path of the writable-path check
240
+ (`agent_can_write`). `on_agent_access: refuse` is the default (maintainer's
241
+ decision, 2026-10-16); `warn` starts; `high_assurance` refuses and does
242
+ not accept `warn`. Unknown is never a different account; an account whose
243
+ groups cannot be listed is in every group (`tests/test_agent_access.py`).
244
+ - Shared ledger (0.7.0): a `Ledger` that finds the file longer than it
245
+ left it reads and verifies the new entries under the cross-process lock
246
+ (chain, head, witness pin, anchor) and does not pass them to its
247
+ observers; a shorter file or a partial line is still refused
248
+ (`tests/test_ledger_shared.py`).
249
+ - Content scanning (0.6.0, `two_key/scan.py`): Two-Key alone starts a scan,
250
+ for every call to a tool whose signed spec maps the scan field, and fills
251
+ the verdict; nothing the agent writes turns it on or off, and `scan` in
252
+ `facts=` is refused (`scan_fact_supplied`). Unavailable is never clean: a
253
+ timeout, an exception, or an invalid result is `unavailable`.
254
+ Could-not-finish (`limits_exceeded`) is flagged. A declared file Two-Key
255
+ cannot read (`file_unreadable`), one over its `max_bytes`
256
+ (`file_too_large`), more than 32 files (`too_many_files`), or more than
257
+ 100 MiB of files in one call (`call_files_too_large`) is denied before
258
+ the scanner and both paths under every stance, monitor included
259
+ (maintainer's decision, 2026-10-15); nothing past a limit is read. Every
260
+ declared file states `max_bytes`, with no default (base64 at most
261
+ 196608). Do not make these denies depend on the stance or give
262
+ `max_bytes` a default. An encrypted file is flagged; only a tool whose
263
+ signed spec maps the scan field with `encrypted: pass` turns an
264
+ `encrypted` finding into the verdict `encrypted`, and only when it is the
265
+ sole finding (with another finding it is flagged; with a part not
266
+ scanned, unavailable). The gateway refuses that verdict for any other
267
+ tool (`scan_mismatch`); an adapter reads only ClamAV's
268
+ `Heuristics.Encrypted` name as encrypted (`tests/test_scan_encrypted.py`).
269
+ Do not make it a `proxy.yaml` setting or a default (maintainer's
270
+ decision, 2026-10-15). The stance is the signed rule. Content never reaches
271
+ the ledger or a reason code, and file content never reaches a judge; a
272
+ flagged call's arguments and proposal reach the judges only with
273
+ `tool_args_on_scan_flag`. The gateway never scans: it checks the token's
274
+ verdict and file digests (`file_changed`). Without a scanner, the judge
275
+ prompts, tokens, and ledger stay byte for byte as before
276
+ (`tests/test_scan.py`). Do not add a scanning engine; adapters
277
+ (`two_key/scanners.py`, standard library only) call external products,
278
+ never keep a threat name or matched text, and read an answer they cannot
279
+ parse as `unavailable`; an ICAP 200 that changes the body is flagged
280
+ (`tests/test_scanners.py`). A `kind: path` file stays inside its declared
281
+ folder (no `..`, no link below it, a regular file), opened through
282
+ `_platform`; do not widen that.
283
+ - The gateway runs in the `TwoKey` process on the `TwoKey` ledger object
284
+ (`TwoKey.gateway()`). A gateway in another process or host is not
285
+ supported (#45); do not add a gateway-only ledger role, remote gateway,
286
+ or cross-process protocol without the operator. Do not describe
287
+ verify-only as key isolation: the process holds the minting, principal,
288
+ witness, and ledger keys, and the registered tools run there.
289
+
290
+ - SIEM export (0.7.0, `two_key/siem.py`, `two_key/siem_sinks.py`,
291
+ `two_key/siem_export.py`): optional, and with no `siem=` nothing is sent
292
+ and nothing changes. What may leave the box is an explicit per-kind
293
+ allowlist (`KINDS`): every body key of every ledger kind is `sent`,
294
+ `hashed`, `excluded`, or extracted, and a key nobody decided fails
295
+ `tests/test_siem.py`. Argument values, proposal and decline text, the
296
+ derived amount, counterparties, custom field values, scanner threat names
297
+ and matched text, and credentials stay excluded, with canary values in the
298
+ tests; add a new ledger field to the allowlist on purpose or not at all.
299
+ The ledger observer only enqueues a sequence number: projecting,
300
+ formatting, and sending happen on the sender thread, so a collector that
301
+ is down, slow, or hostile never blocks, delays, or changes an
302
+ authorization, and a full queue drops rather than waits. TLS sets
303
+ TLS 1.2 as the minimum and always verifies the certificate and the host
304
+ name; do not add a setting that turns either off. A plaintext sink (`udp`,
305
+ `tcp`) is refused under `high_assurance` (`siem_plaintext_refused`) and
306
+ warns at start otherwise. A webhook never follows a redirect and reads its
307
+ authorization header and HMAC key from owner-only files through
308
+ `_platform`. A token the gateway refuses is still not ledgered: it only
309
+ increments an in-memory class counter, reported at most once an interval
310
+ with no token text, digest, or id (`ledgered: false`, `seq: -1`).
311
+ `two-key siem-export` verifies the ledger first, writes a new owner-only
312
+ file and refuses to overwrite, and `two-key siem-verify` re-derives every
313
+ event from the ledger (`tests/test_siem_export.py`). Keep the unit tests
314
+ network-free: loopback only, with `tests/tls.py` for certificates.
315
+
316
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the human contributor checklist.