interbolt 0.1.0__tar.gz → 0.2.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 (119) hide show
  1. {interbolt-0.1.0 → interbolt-0.2.0}/.github/workflows/ci.yml +2 -3
  2. {interbolt-0.1.0 → interbolt-0.2.0}/.github/workflows/release.yml +18 -3
  3. {interbolt-0.1.0 → interbolt-0.2.0}/.gitignore +1 -0
  4. {interbolt-0.1.0 → interbolt-0.2.0}/.pre-commit-config.yaml +2 -1
  5. interbolt-0.2.0/ARCHITECTURE.md +215 -0
  6. interbolt-0.2.0/CHANGELOG.md +117 -0
  7. interbolt-0.2.0/CLAUDE.md +53 -0
  8. interbolt-0.2.0/PKG-INFO +272 -0
  9. interbolt-0.2.0/README.md +240 -0
  10. interbolt-0.2.0/bench.py +120 -0
  11. {interbolt-0.1.0 → interbolt-0.2.0}/cliff.toml +9 -1
  12. {interbolt-0.1.0 → interbolt-0.2.0}/pyproject.toml +29 -10
  13. interbolt-0.2.0/src/interbolt/__init__.py +164 -0
  14. interbolt-0.2.0/src/interbolt/cli/__init__.py +7 -0
  15. interbolt-0.2.0/src/interbolt/cli/commands.py +169 -0
  16. interbolt-0.2.0/src/interbolt/cli/main.py +76 -0
  17. interbolt-0.2.0/src/interbolt/cli/render.py +131 -0
  18. {interbolt-0.1.0 → interbolt-0.2.0}/src/interbolt/constants.py +29 -4
  19. interbolt-0.2.0/src/interbolt/enforcement/__init__.py +10 -0
  20. interbolt-0.2.0/src/interbolt/enforcement/audit.py +172 -0
  21. interbolt-0.2.0/src/interbolt/enforcement/check.py +292 -0
  22. interbolt-0.2.0/src/interbolt/enforcement/enforce.py +91 -0
  23. interbolt-0.2.0/src/interbolt/enforcement/signals.py +52 -0
  24. {interbolt-0.1.0 → interbolt-0.2.0}/src/interbolt/errors.py +7 -4
  25. interbolt-0.2.0/src/interbolt/models/__init__.py +1 -0
  26. interbolt-0.2.0/src/interbolt/models/core.py +213 -0
  27. interbolt-0.2.0/src/interbolt/models/protocols.py +43 -0
  28. interbolt-0.2.0/src/interbolt/policy/__init__.py +37 -0
  29. interbolt-0.2.0/src/interbolt/policy/cel.py +97 -0
  30. interbolt-0.2.0/src/interbolt/policy/compile.py +80 -0
  31. interbolt-0.2.0/src/interbolt/policy/evaluate.py +262 -0
  32. interbolt-0.2.0/src/interbolt/policy/explain.py +310 -0
  33. interbolt-0.2.0/src/interbolt/policy/identity_ast.py +222 -0
  34. interbolt-0.2.0/src/interbolt/policy/partial_eval.py +201 -0
  35. interbolt-0.2.0/src/interbolt/policy/policy.py +114 -0
  36. {interbolt-0.1.0 → interbolt-0.2.0}/src/interbolt/policy/schema.json +57 -13
  37. interbolt-0.2.0/src/interbolt/policy/schema.py +517 -0
  38. interbolt-0.2.0/src/interbolt/policy/shadowing.py +213 -0
  39. interbolt-0.2.0/src/interbolt/policy.example.yaml +91 -0
  40. interbolt-0.2.0/src/interbolt/py.typed +0 -0
  41. interbolt-0.2.0/src/interbolt/reporting/__init__.py +13 -0
  42. interbolt-0.2.0/src/interbolt/reporting/describe.py +106 -0
  43. interbolt-0.2.0/src/interbolt/reporting/otel.py +138 -0
  44. interbolt-0.2.0/src/interbolt/reporting/reporters.py +180 -0
  45. interbolt-0.2.0/src/interbolt/runtime/__init__.py +16 -0
  46. interbolt-0.2.0/src/interbolt/runtime/config.py +175 -0
  47. interbolt-0.2.0/src/interbolt/runtime/current.py +55 -0
  48. interbolt-0.2.0/src/interbolt/runtime/guard.py +353 -0
  49. interbolt-0.2.0/src/interbolt/runtime/observers.py +67 -0
  50. interbolt-0.2.0/src/interbolt/runtime/runtime.py +253 -0
  51. interbolt-0.2.0/src/interbolt/taint/__init__.py +25 -0
  52. interbolt-0.2.0/src/interbolt/taint/carriers.py +503 -0
  53. interbolt-0.2.0/src/interbolt/taint/endorse.py +142 -0
  54. interbolt-0.2.0/src/interbolt/taint/ingress.py +205 -0
  55. interbolt-0.2.0/src/interbolt/taint/runstate.py +108 -0
  56. interbolt-0.2.0/src/interbolt/taint/walk.py +175 -0
  57. interbolt-0.2.0/src/interbolt/taint/wire.py +280 -0
  58. interbolt-0.2.0/src/interbolt/taint/wire_rebuild.py +146 -0
  59. interbolt-0.2.0/src/interbolt/taint/wire_schema.py +293 -0
  60. interbolt-0.2.0/src/interbolt/taint/wire_walk.py +280 -0
  61. interbolt-0.2.0/src/interbolt/utils/__init__.py +20 -0
  62. interbolt-0.2.0/src/interbolt/utils/context.py +63 -0
  63. interbolt-0.2.0/src/interbolt/utils/log.py +21 -0
  64. interbolt-0.2.0/src/interbolt/utils/names.py +110 -0
  65. interbolt-0.2.0/src/interbolt/utils/signatures.py +20 -0
  66. interbolt-0.2.0/tests/architecture/test_layering.py +53 -0
  67. interbolt-0.2.0/tests/conftest.py +162 -0
  68. interbolt-0.2.0/tests/integration/fixtures/__init__.py +10 -0
  69. interbolt-0.2.0/tests/integration/fixtures/agents.py +15 -0
  70. interbolt-0.2.0/tests/integration/fixtures/model.py +11 -0
  71. interbolt-0.2.0/tests/integration/fixtures/sources.py +19 -0
  72. interbolt-0.2.0/tests/integration/fixtures/tools.py +26 -0
  73. {interbolt-0.1.0 → interbolt-0.2.0}/tests/integration/test_agent_loop.py +78 -1
  74. interbolt-0.2.0/tests/integration/test_audit.py +156 -0
  75. interbolt-0.2.0/tests/integration/test_concurrency.py +145 -0
  76. interbolt-0.2.0/tests/integration/test_endorse.py +105 -0
  77. interbolt-0.2.0/tests/integration/test_multi_module_agent.py +93 -0
  78. interbolt-0.2.0/tests/integration/test_serialization.py +190 -0
  79. {interbolt-0.1.0 → interbolt-0.2.0}/tests/policies/agent_loop.yaml +2 -3
  80. interbolt-0.2.0/tests/unit/__init__.py +0 -0
  81. interbolt-0.2.0/tests/unit/test_cli.py +582 -0
  82. interbolt-0.2.0/tests/unit/test_constants.py +73 -0
  83. interbolt-0.2.0/tests/unit/test_enforcement.py +1289 -0
  84. interbolt-0.2.0/tests/unit/test_errors.py +101 -0
  85. interbolt-0.2.0/tests/unit/test_models.py +140 -0
  86. interbolt-0.2.0/tests/unit/test_policy_engine.py +1003 -0
  87. interbolt-0.2.0/tests/unit/test_policy_explain.py +367 -0
  88. interbolt-0.2.0/tests/unit/test_policy_schema.py +1188 -0
  89. interbolt-0.2.0/tests/unit/test_reporting.py +349 -0
  90. interbolt-0.2.0/tests/unit/test_reporting_otel.py +276 -0
  91. interbolt-0.2.0/tests/unit/test_runtime.py +956 -0
  92. interbolt-0.2.0/tests/unit/test_runtime_guard.py +401 -0
  93. interbolt-0.2.0/tests/unit/test_taint.py +1316 -0
  94. interbolt-0.2.0/tests/unit/test_taint_stress.py +79 -0
  95. interbolt-0.2.0/tests/unit/test_taint_wire.py +702 -0
  96. interbolt-0.2.0/tests/unit/test_utils.py +75 -0
  97. {interbolt-0.1.0 → interbolt-0.2.0}/uv.lock +48 -2
  98. interbolt-0.1.0/CHANGELOG.md +0 -5
  99. interbolt-0.1.0/CLAUDE.md +0 -77
  100. interbolt-0.1.0/PKG-INFO +0 -104
  101. interbolt-0.1.0/README.md +0 -73
  102. interbolt-0.1.0/policy.example.yaml +0 -54
  103. interbolt-0.1.0/src/interbolt/__init__.py +0 -55
  104. interbolt-0.1.0/src/interbolt/cli/__init__.py +0 -43
  105. interbolt-0.1.0/src/interbolt/enforcement/__init__.py +0 -302
  106. interbolt-0.1.0/src/interbolt/models/__init__.py +0 -1
  107. interbolt-0.1.0/src/interbolt/models/core.py +0 -162
  108. interbolt-0.1.0/src/interbolt/models/protocols.py +0 -56
  109. interbolt-0.1.0/src/interbolt/policy/__init__.py +0 -61
  110. interbolt-0.1.0/src/interbolt/policy/engine.py +0 -205
  111. interbolt-0.1.0/src/interbolt/policy/schema.py +0 -172
  112. interbolt-0.1.0/src/interbolt/reporting/__init__.py +0 -57
  113. interbolt-0.1.0/src/interbolt/runtime/__init__.py +0 -259
  114. interbolt-0.1.0/src/interbolt/runtime/guard.py +0 -176
  115. interbolt-0.1.0/src/interbolt/taint/__init__.py +0 -355
  116. interbolt-0.1.0/src/interbolt/utils/__init__.py +0 -43
  117. interbolt-0.1.0/tests/conftest.py +0 -33
  118. {interbolt-0.1.0 → interbolt-0.2.0}/.python-version +0 -0
  119. {interbolt-0.1.0 → interbolt-0.2.0}/LICENSE +0 -0
@@ -64,9 +64,8 @@ jobs:
64
64
  - name: Type Check (Mypy)
65
65
  run: uv run mypy .
66
66
 
67
- # TODO: Add in when adding unit tests
68
- # - name: Run Unit Tests
69
- # run: uv run pytest tests/unit/ --cov=interbolt --cov-report=xml
67
+ - name: Run Unit Tests
68
+ run: uv run pytest tests/unit/ --cov=interbolt --cov-report=xml
70
69
 
71
70
  - name: Run Integration Tests
72
71
  run: uv run pytest tests/integration/ -v --no-cov
@@ -71,8 +71,8 @@ jobs:
71
71
  # 2. Check if the version string is present in the file
72
72
  if ! grep -Fq "${{ inputs.version }}" CHANGELOG.md; then
73
73
  echo "ERROR: CHANGELOG.md does not contain version ${{ inputs.version }}."
74
- echo "Please run locally: uv run git-cliff --tag ${{ inputs.version }} --output CHANGELOG.md"
75
- echo "Then commit and push the changes."
74
+ echo "Please run locally: git cliff --unreleased --tag ${{ inputs.version }} --prepend CHANGELOG.md"
75
+ echo "Then hand-edit the new section, commit, and push."
76
76
  exit 1
77
77
  fi
78
78
  echo "SUCCESS: Found version ${{ inputs.version }} in CHANGELOG.md"
@@ -97,4 +97,19 @@ jobs:
97
97
  git config user.name "GitHub Actions"
98
98
  git config user.email "actions@github.com"
99
99
  git tag -a "v${{ inputs.version }}" -m "Release v${{ inputs.version }}"
100
- git push origin "v${{ inputs.version }}"
100
+ git push origin "v${{ inputs.version }}"
101
+
102
+ - name: Create GitHub Release
103
+ env:
104
+ GH_TOKEN: ${{ github.token }}
105
+ run: |
106
+ # Pull just this version's section out of CHANGELOG.md for the body
107
+ awk -v ver="## [${{ inputs.version }}]" '
108
+ index($0, ver) == 1 { flag = 1; next }
109
+ /^## \[/ && flag { exit }
110
+ flag { print }
111
+ ' CHANGELOG.md > release-notes.md
112
+ gh release create "v${{ inputs.version }}" \
113
+ --title "v${{ inputs.version }}" \
114
+ --notes-file release-notes.md \
115
+ --verify-tag
@@ -39,3 +39,4 @@ htmlcov/
39
39
 
40
40
  # --- Application ---
41
41
  dev/
42
+ .claude/
@@ -7,9 +7,10 @@ repos:
7
7
  - id: ruff-format
8
8
 
9
9
  - repo: https://github.com/pre-commit/mirrors-mypy
10
- rev: v1.14.1
10
+ rev: v2.1.0
11
11
  hooks:
12
12
  - id: mypy
13
+ args: [--config-file=pyproject.toml]
13
14
  additional_dependencies:
14
15
  [
15
16
  pydantic,
@@ -0,0 +1,215 @@
1
+ # Architecture
2
+
3
+ How Interbolt is put together, and the properties the implementation is
4
+ required to hold. Written for someone reading or changing the source. For usage,
5
+ see the [documentation](https://docs.deconvolutelabs.com/docs).
6
+
7
+ Every rule here is a constraint on the code. If the code and this document
8
+ disagree, one of them is a bug.
9
+
10
+ ## The decision path
11
+
12
+ One guarded tool call runs the following sequence. Reading it in order is the
13
+ fastest way to orient in the codebase.
14
+
15
+ 1. **Ingress.** `taint(value, source="web_search")` in `taint/ingress.py` wraps
16
+ the value in a carrier from `taint/carriers.py` and attaches a `Label`
17
+ recording the source name. The source is recorded against the active run in
18
+ `taint/runstate.py`. No trust is decided here.
19
+ 2. **Propagation.** The carrier moves through application code, overriding the
20
+ operations in the propagation contract so the label survives them.
21
+ Everything else produces a plain, unlabeled value.
22
+ 3. **The boundary.** `@guard` in `runtime/guard.py` binds the call's arguments
23
+ with `inspect.signature`, resolves the acting agent, and calls
24
+ `Runtime.check`.
25
+ 4. **Collection.** `enforcement/check.py` walks the bound arguments to a bounded
26
+ depth, collecting every label (`taint/walk.py: collect_labels`) and producing
27
+ a carrier-free copy for CEL (`unwrap`).
28
+ 5. **Resolution.** `policy/evaluate.py: resolve_labels` resolves each label's
29
+ lineage against the policy's `sources` table exactly once. A source absent
30
+ from the table resolves untrusted. The trifecta leg, the untrusted source
31
+ set, and the CEL `taint` list all derive from that single resolution.
32
+ 6. **Evaluation.** The sink's precompiled CEL rules run in order, first match
33
+ wins. No match falls through to `defaults.sink_action`.
34
+ 7. **Mode.** `_apply_mode` maps an evaluation error to block under `enforce` and
35
+ to allow under `monitor`, and downgrades everything to allow under `dry_run`.
36
+ `Outcome` records what evaluation computed, before that downgrade;
37
+ `Decision.action` records what was enforced.
38
+ 8. **Emission.** A `Decision` and an `Event` are handed to the reporter.
39
+ Emission is fire-and-forget and never affects the decision.
40
+ 9. **Enforcement.** `enforcement/enforce.py` turns the `Decision` into control
41
+ flow: return on allow, raise `PolicyViolation` on block, consult the
42
+ `ApprovalResolver` on require-approval.
43
+
44
+ Steps 4 through 8 are `check()`. Step 9 is `enforce_decision`. `guard` does both.
45
+
46
+ ## Layers
47
+
48
+ Imports point inward along this order. Nothing reaches outward.
49
+
50
+ ```
51
+ errors.py, constants.py, utils/ leaves
52
+ |
53
+ models/ Pydantic models and Protocols only
54
+ |
55
+ taint/ policy/ independent of each other
56
+ \ /
57
+ enforcement/ the decision core
58
+ |
59
+ reporting/
60
+ |
61
+ runtime/ the composition root
62
+ |
63
+ cli/, integrations/ thin edges
64
+ ```
65
+
66
+ `tests/unit/test_architecture_invariants.py` enforces this by parsing every
67
+ import in `src/interbolt`. The rules it cannot check:
68
+
69
+ - Leaves import only the standard library. `constants.py` and `utils/` may
70
+ import `errors.py`. `errors.py` references `Decision` under `TYPE_CHECKING`
71
+ only. `utils/` may resolve `opentelemetry.trace` through a guarded, cached
72
+ `try/except ImportError`, which is never a hard dependency.
73
+ - `models/` holds Pydantic models and Protocols. Validation, parsing, and name
74
+ helpers live in `utils/`, not here.
75
+ - `taint/` and `policy/` never import each other. This is why
76
+ `policy/evaluate.py` handles only `str`, `bytes`, and containers: it receives
77
+ arguments with carriers already stripped.
78
+ - `enforcement/` emits through the `Reporter` protocol in
79
+ `models/protocols.py` and never imports `reporting/`.
80
+ - `runtime/` is the composition root. Nothing imports it except the package
81
+ `__init__`.
82
+ - `cli/` imports the public surface and never reaches into a package's
83
+ internals. The same rule governs `integrations/`, which is planned and not yet
84
+ present: framework glue lives there behind an optional extra, never in core.
85
+ - `from __future__ import annotations` at the top of every module. Cross-layer
86
+ type-only references use `TYPE_CHECKING`-guarded imports.
87
+
88
+ ## Module map
89
+
90
+ **`taint/`** carries provenance.
91
+
92
+ | Module | Contents |
93
+ | --- | --- |
94
+ | `carriers.py` | `Tainted`, `TaintedBytes`, `LabeledValue`, label merge |
95
+ | `ingress.py` | `taint()`, `track_model_call` |
96
+ | `endorse.py` | `endorse()` and its record emission |
97
+ | `walk.py` | depth-bounded leaf traversal, used at ingress and the sink |
98
+ | `runstate.py` | run-ingress registry and the two extension hooks |
99
+ | `wire*.py` | the `pack`/`unpack` serialization contract |
100
+
101
+ **`policy/`** loads, compiles, evaluates, and statically analyzes policy.
102
+
103
+ | Module | Contents |
104
+ | --- | --- |
105
+ | `schema.py` | the Pydantic policy document, validation, fingerprinting |
106
+ | `cel.py` | CEL parsing and compilation helpers |
107
+ | `compile.py` | one-time compilation of every sink's rule list |
108
+ | `evaluate.py` | per-call trust resolution, CEL context, sink evaluation |
109
+ | `policy.py` | the `Policy` class and the built-in default |
110
+ | `identity_ast.py`, `shadowing.py`, `partial_eval.py`, `explain.py` | static analysis behind `interbolt explain` and the unreachable-rule check |
111
+
112
+ **`enforcement/`** is the decision core: `check.py` (the pipeline), `signals.py`
113
+ (trust signals derived once per call), `enforce.py` (decision to control flow),
114
+ `audit.py` (the laundering audit registry).
115
+
116
+ **`runtime/`** composes everything: `config.py` (`configure()`), `runtime.py`
117
+ (the `Runtime` class), `current.py` (the process-current runtime), `guard.py`
118
+ (`@guard`, `check`, `agent`, agent-id validation), `observers.py` (the hooks
119
+ `configure()` installs).
120
+
121
+ **`reporting/`** holds the shipped `Reporter` implementations, the `describe_*`
122
+ formatters, and the optional OpenTelemetry reporter.
123
+
124
+ ## Invariants
125
+
126
+ - `taint()` records only the source name. Trust resolves at the sink from the
127
+ policy's `sources` table. A resolved `TrustLevel` is never stored on a `Label`.
128
+ - `check()` is the single decision entrypoint. `guard` is sugar over it.
129
+ - Mode governs only behavior on evaluation error and whether blocks are real. A
130
+ correct block or require-approval always acts, except under `dry_run`.
131
+ - The default posture is deny. An undeclared source is untrusted. A sink with no
132
+ matching rule falls through to `defaults.sink_action`.
133
+ - Rule evaluation is first-match-wins within a sink's ordered list. Rules are
134
+ never reordered at load time.
135
+ - Policies and CEL expressions compile once at load. Nothing compiles inside
136
+ `check()`.
137
+ - Tool identity is the structured `(namespace, tool)` pair internally; the
138
+ dotted form is surface only. A namespace or tool containing a dot is rejected.
139
+ - Every `Decision` carries `agent_id`, `run_id`, and optional `session_id`. A
140
+ durable `agent_id` is never fabricated.
141
+ - `agent_id` is never accepted from a taint carrier. It is an authorization
142
+ input once a policy reads `agent.id`, so it comes from deterministic dispatch
143
+ rather than model output. Charset, carrier rejection, and rejection of the
144
+ reserved `"default"` apply where identity is bound; charset and carrier
145
+ rejection alone apply inside `Runtime.check`, the chokepoint every path
146
+ funnels through.
147
+ - An approval authorizes exactly one call. It is never cached, persisted, or
148
+ reused, not across runs and not for the next call in the same run.
149
+ - There is no principal-level elevation primitive. Group membership is fixed on
150
+ a live runtime and changes only by a new `configure()` call.
151
+ - Effective mode comes from the first of `INTERBOLT_MODE`, the policy's
152
+ `defaults.fail_mode`, then `configure(mode=)`. Every override logs a warning.
153
+ - `Event` embeds its `Decision` and duplicates nothing from it. Changing a
154
+ record shape means bumping `constants.EVENT_SCHEMA_VERSION`; changing the
155
+ `pack` envelope means bumping `WIRE_SCHEMA_VERSION`.
156
+ - `configure()` has no import-time side effects, and is the only function that
157
+ installs process-global state.
158
+ - Reporter emission is fire-and-forget. A reporter failure never affects,
159
+ delays, or fails a decision.
160
+ - The core makes no network calls under any default configuration.
161
+ - All exceptions come from `errors.py` under `InterboltError`, in two branches:
162
+ decision outcomes (`PolicyViolation`, `PolicyEvaluationError`,
163
+ `ApprovalDenied`) and misuse (`InterboltConfigError`, `InterboltUsageError`).
164
+ The misuse pair also inherits the fitting builtin. The library never raises a
165
+ bare `Exception`, `ValueError`, or `RuntimeError`.
166
+
167
+ ## Two rules that are easy to violate by accident
168
+
169
+ **Container traversal exists in exactly two places.** `taint/walk.py` is
170
+ depth-bounded and leaf-oriented, used at ingress and the sink.
171
+ `taint/wire_walk.py` is path-keyed, used by the serialization contract.
172
+ `wire.py` and `wire_rebuild.py` build entirely on `wire_walk.py`'s primitives
173
+ and descend nothing themselves. Do not add a third.
174
+
175
+ **Mutable module-level state exists in exactly two modules.**
176
+ `taint/runstate.py` holds the run-ingress registry and the two hooks;
177
+ `runtime/current.py` holds the process-current runtime. Both hooks exist so
178
+ `runtime/` can wire behavior into `taint/` without `taint/` importing upward.
179
+ Read a global through its owning module's getter, never with
180
+ `from x import _the_variable`, which binds a stale snapshot. Do not add a third
181
+ module holding state.
182
+
183
+ Identity binding uses `ContextVar`s in `utils/context.py`, which do not cross a
184
+ thread boundary. A guarded call on a thread pool needs `agent(...)`, and a
185
+ `taint()` call on an offloaded thread is invisible to that run's `run.tainted`.
186
+
187
+ ## Where the rest lives
188
+
189
+ - **The propagation contract**, stating exactly what survives which operation:
190
+ [taint propagation](https://docs.deconvolutelabs.com/docs/concepts/taint-propagation).
191
+ Summary: labels survive direct passing and operator-style combination, and are
192
+ lost to f-strings with literal text, `str.format` on a plain template, and
193
+ `join` on a plain separator.
194
+ - **Design limits and what is not a vulnerability**: [SECURITY.md](SECURITY.md).
195
+ The one worth knowing before writing a policy is that only the
196
+ `from_untrusted` trifecta leg is computed, so `trifecta.size` never exceeds
197
+ one and rules must be written directly against `taint` and `args`.
198
+ - **Record schemas and the OTel mapping**:
199
+ [events](https://docs.deconvolutelabs.com/docs/reference/events).
200
+ - **Policy internals**, including the CEL context shape and what `validate`
201
+ does and does not catch:
202
+ [policy internals](https://docs.deconvolutelabs.com/docs/reference/policy-internals).
203
+ - **Style, testing, and the local check loop**: [CONTRIBUTING.md](CONTRIBUTING.md).
204
+
205
+ ## Adding code
206
+
207
+ - A package `__init__.py` holds the package docstring and re-exports only.
208
+ - Split a module past roughly 300 lines, along the conceptual seam rather than
209
+ at the line count. `taint/carriers.py` is a standing exemption: its
210
+ `Tainted`/`TaintedBytes` symmetry is deliberate, because the propagation
211
+ contract is the security surface and a reader auditing whether `.replace()`
212
+ propagates should find a method rather than a metaclass.
213
+ - Constants live in `constants.py` if global, or in the owning layer.
214
+ - No agent framework imports in the core.
215
+ - Type hints on every signature. `Any` needs an inline comment explaining why.
@@ -0,0 +1,117 @@
1
+ ## [0.2.0] - 2026-07-26
2
+
3
+ ### ⚠️ Breaking Changes
4
+
5
+ - **Policy conditions are plain CEL; the `.any(` alias is removed.** Change
6
+ `.any(` to `.exists(` in every `when:` expression. A policy still using it
7
+ now fails at load with `InterboltConfigError` and at `interbolt validate`
8
+ with a message naming the fix, rather than failing at evaluation time.
9
+ - **`Event` no longer duplicates fields from its embedded `Decision`.**
10
+ `agent_id`, `run_id`, `session_id`, `matched_rule`, `mode`, `run_tainted`,
11
+ `trifecta`, and `untrusted_sources` are gone from `Event`; read them via
12
+ `event.decision.<field>`. `Event.lineage` is also removed, having been
13
+ redundant with `Event.sources`.
14
+ - **`Event.outcome` is now the `Outcome` enum** rather than a raw string. It
15
+ is a `StrEnum`, so `event.outcome == "block"` still works.
16
+ - **`defaults.source_trust` is removed from the policy schema.** No code path
17
+ ever read it; undeclared sources have always resolved untrusted. Because
18
+ `Defaults` now forbids unknown keys, a policy still carrying it fails to
19
+ load instead of loading and silently doing nothing. Delete the line.
20
+ - **`EVENT_SCHEMA_VERSION` moves from 6 to 9.** Anything parsing a
21
+ `JsonlReporter` log should read it and fail loudly on an unrecognized value.
22
+
23
+ ### 🔒 Security
24
+
25
+ - **Fixed a silent fail-open in bare `check()`.** Because the run-ingress
26
+ registry keys on `run_id` and `check()` minted a fresh one whenever the
27
+ caller passed `None`, a `check()` call made inside an active `agent_context`
28
+ reported `run.tainted` as false even when the run had ingested untrusted
29
+ data. Any `run.tainted`-gated rule silently permitted the call. `check()`
30
+ now resolves `run_id` from the ambient context, minting one only when no run
31
+ is active. Custom dispatch loops using `check()` directly were affected but
32
+ `@guard` was not.
33
+ - **Fixed string-literal false positives and false negatives in
34
+ `interbolt validate`.** Text-level lints treated the contents of CEL string
35
+ literals as code, so a valid condition such as
36
+ `args.path == "/etc/agent.conf"` failed validation with an error about a
37
+ field the author never wrote, while a literal containing the word `sources`
38
+ suppressed the identity-only-allow safety warning. The reference checks now
39
+ run on the parsed CEL AST.
40
+
41
+ ### 🚀 Features
42
+
43
+ - **Cross-boundary provenance.** `pack`, `unpack`, `pack_into`, and
44
+ `unpack_from` carry a value's labels and run-scoped ingress across a
45
+ serialization or process boundary in a versioned, optionally
46
+ MAC-authenticated envelope, at `WIRE_SCHEMA_VERSION` 1. Every other channel
47
+ still resets to fresh untrusted ingress.
48
+ - **`interbolt explain`.** Answers what a given agent, group, or tool can
49
+ actually reach, resolving each sink's rules against one identity and
50
+ reporting unreachable rules. Backed by `explain_for_agent`,
51
+ `explain_for_group`, and `explain_for_tool` on the public surface.
52
+ - **Agent identity in policy.** `agent.id` and `agent.groups` are available in
53
+ CEL conditions, with group membership declared in a new optional `agents:`
54
+ policy section, so a per-agent carve-out can live in one policy file.
55
+ - **`enforce_decision` and `enforce_decision_sync`.** Turn a `Decision` into
56
+ control flow at a call site `@guard` cannot decorate, such as a
57
+ framework-owned tool executor or an MCP proxy.
58
+ - **`Label.ingested_by`**, recording which agents ingested or derived a value,
59
+ readable in CEL as `t.ingested_by`.
60
+ - **`policy_fingerprint` on every emitted record**, a stable hash of the
61
+ policy in force, so a stored record stays attributable after the policy
62
+ changes.
63
+
64
+ ### 📚 Documentation
65
+
66
+ - Added `ARCHITECTURE.md`.
67
+ - Rewrote the README against the code, with an explicit pre-1.0 stability
68
+ statement: the public API may change in any `0.x` minor.
69
+ - Add Interbolt-for-OTel-users guide and record integration wrapper decision
70
+
71
+ ### 🧪 Testing
72
+
73
+ - Enforce the layering rule mechanically
74
+
75
+ ### ⚙️ Miscellaneous Tasks
76
+
77
+ - Improve docstrings and comments
78
+ - Update release workflow and tooling
79
+
80
+ ## [0.1.1] - 2026-07-15
81
+
82
+ ### 🚀 Features
83
+
84
+ - Add unit tests
85
+ - Add init policy handling
86
+ - Add new guard api
87
+ - Add taint for run-level
88
+ - Add better logging
89
+ - Add graph tool
90
+ - Add improved reporters
91
+ - Add extensible reporters via add_reporter and get_runtime
92
+ - Fix taint-tracking correctness and propagation path efficiency
93
+ - OpenTelemetry integration via OTelReporter and trace-context join keys
94
+
95
+ ### 🐛 Bug Fixes
96
+
97
+ - Correctness fixes for CEL rewrite, audit registration, and container traversal
98
+
99
+ ### 📚 Documentation
100
+
101
+ - Add first draft
102
+
103
+ ### ⚙️ Miscellaneous Tasks
104
+
105
+ - Clean up code
106
+ - Improve model
107
+ - Update docstrings
108
+
109
+ ## [0.1.0] - 2026-06-30
110
+
111
+ ### 🚀 Features
112
+
113
+ - Init commit
114
+
115
+ ### ⚙️ Miscellaneous Tasks
116
+
117
+ - Prepare v0.1.0
@@ -0,0 +1,53 @@
1
+ # Working in this repository
2
+
3
+ ## Read first
4
+
5
+ [ARCHITECTURE.md](ARCHITECTURE.md) governs. Read in full before writing code. Carry the layering rules, the invariants, the propagation contract, the testing conventions, and
6
+ the style rules, and they are the authority wherever anything below overlaps.
7
+
8
+ If `dev/spec.md` exists in the working tree, read it too and treat it as
9
+ governing wherever it overlaps. If it does not exist, do not ask for it,
10
+ reference it, or block on it.
11
+
12
+ ## Before any change is done
13
+
14
+ ```bash
15
+ uv run ruff format --check .
16
+ uv run ruff check .
17
+ uv run mypy .
18
+ uv run pytest
19
+ ```
20
+
21
+ All four pass, or the change is not finished.
22
+
23
+ ## Rules specific to working here with an agent
24
+
25
+ - **Do not implement tests unless explicitly instructed.** When you do, follow
26
+ the conventions in CONTRIBUTING.md rather than building a harness.
27
+ - **A wall-clock stress test that fails under coverage but passes in isolation
28
+ is a timing flake.** Do not "fix" it by changing library code.
29
+ - **Never weaken the propagation contract or the default-deny posture as a side
30
+ effect of another change.** If a change alters what propagates, what a
31
+ missing rule falls through to, or what an undeclared source resolves to, stop
32
+ and say so explicitly rather than absorbing it into a larger diff.
33
+ - **A docstring that describes behavior the code does not have is a defect of
34
+ the same kind as a wrong branch.** In a library whose value is that its
35
+ guarantees are precise, do not write an aspirational docstring.
36
+ - **Docstrings and comments never justify themselves.** No references to an
37
+ internal design document, a ticket or PR number, or a line-count threshold.
38
+ Do not explain why something is not done, why an alternative was rejected, or
39
+ otherwise narrate a design discussion. State what the code does and, where it
40
+ genuinely helps a reader, why it does it that way, in a way that stands on
41
+ its own.
42
+ - **Record shapes are versioned.** Changing `Event`, `Finding`, `Endorsement`,
43
+ or `Label` means bumping `constants.EVENT_SCHEMA_VERSION`. Changing the
44
+ `pack`/`unpack` envelope means bumping `constants.WIRE_SCHEMA_VERSION`.
45
+ - **Do not add a mutable module-level global.** The two modules permitted to
46
+ hold process-global state are named in ARCHITECTURE.md.
47
+ - **Do not add a third container walk.** The two permitted traversals are named
48
+ in ARCHITECTURE.md.
49
+
50
+ ## Style
51
+
52
+ American English. No em dash, no double dash. No `print()`: CLI output goes
53
+ through `rich.console.Console`, log output through the library logger.