interbolt 0.1.1__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.
- {interbolt-0.1.1 → interbolt-0.2.0}/.github/workflows/release.yml +18 -3
- {interbolt-0.1.1 → interbolt-0.2.0}/.gitignore +2 -1
- interbolt-0.2.0/ARCHITECTURE.md +215 -0
- interbolt-0.2.0/CHANGELOG.md +117 -0
- interbolt-0.2.0/CLAUDE.md +53 -0
- interbolt-0.2.0/PKG-INFO +272 -0
- interbolt-0.2.0/README.md +240 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/bench.py +5 -2
- {interbolt-0.1.1 → interbolt-0.2.0}/cliff.toml +9 -1
- {interbolt-0.1.1 → interbolt-0.2.0}/pyproject.toml +14 -10
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/__init__.py +45 -3
- interbolt-0.2.0/src/interbolt/cli/__init__.py +7 -0
- interbolt-0.1.1/src/interbolt/cli/__init__.py → interbolt-0.2.0/src/interbolt/cli/commands.py +64 -94
- interbolt-0.2.0/src/interbolt/cli/main.py +76 -0
- interbolt-0.2.0/src/interbolt/cli/render.py +131 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/constants.py +13 -6
- interbolt-0.2.0/src/interbolt/enforcement/__init__.py +10 -0
- interbolt-0.2.0/src/interbolt/enforcement/audit.py +172 -0
- interbolt-0.2.0/src/interbolt/enforcement/check.py +292 -0
- interbolt-0.2.0/src/interbolt/enforcement/enforce.py +91 -0
- interbolt-0.2.0/src/interbolt/enforcement/signals.py +52 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/errors.py +5 -0
- interbolt-0.2.0/src/interbolt/models/__init__.py +1 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/models/core.py +72 -97
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/models/protocols.py +5 -15
- interbolt-0.2.0/src/interbolt/policy/__init__.py +37 -0
- interbolt-0.2.0/src/interbolt/policy/cel.py +97 -0
- interbolt-0.2.0/src/interbolt/policy/compile.py +80 -0
- interbolt-0.1.1/src/interbolt/policy/engine.py → interbolt-0.2.0/src/interbolt/policy/evaluate.py +62 -136
- interbolt-0.2.0/src/interbolt/policy/explain.py +310 -0
- interbolt-0.2.0/src/interbolt/policy/identity_ast.py +222 -0
- interbolt-0.2.0/src/interbolt/policy/partial_eval.py +201 -0
- interbolt-0.1.1/src/interbolt/policy/__init__.py → interbolt-0.2.0/src/interbolt/policy/policy.py +36 -13
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/policy/schema.json +57 -13
- interbolt-0.2.0/src/interbolt/policy/schema.py +517 -0
- interbolt-0.2.0/src/interbolt/policy/shadowing.py +213 -0
- interbolt-0.2.0/src/interbolt/policy.example.yaml +91 -0
- interbolt-0.2.0/src/interbolt/reporting/__init__.py +13 -0
- interbolt-0.2.0/src/interbolt/reporting/describe.py +106 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/reporting/otel.py +18 -16
- interbolt-0.1.1/src/interbolt/reporting/__init__.py → interbolt-0.2.0/src/interbolt/reporting/reporters.py +18 -99
- interbolt-0.2.0/src/interbolt/runtime/__init__.py +16 -0
- interbolt-0.2.0/src/interbolt/runtime/config.py +175 -0
- interbolt-0.2.0/src/interbolt/runtime/current.py +55 -0
- interbolt-0.2.0/src/interbolt/runtime/guard.py +353 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/runtime/observers.py +9 -1
- interbolt-0.2.0/src/interbolt/runtime/runtime.py +253 -0
- interbolt-0.2.0/src/interbolt/taint/__init__.py +25 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/src/interbolt/taint/carriers.py +38 -30
- interbolt-0.2.0/src/interbolt/taint/endorse.py +142 -0
- interbolt-0.2.0/src/interbolt/taint/ingress.py +205 -0
- interbolt-0.2.0/src/interbolt/taint/runstate.py +108 -0
- interbolt-0.2.0/src/interbolt/taint/walk.py +175 -0
- interbolt-0.2.0/src/interbolt/taint/wire.py +280 -0
- interbolt-0.2.0/src/interbolt/taint/wire_rebuild.py +146 -0
- interbolt-0.2.0/src/interbolt/taint/wire_schema.py +293 -0
- interbolt-0.2.0/src/interbolt/taint/wire_walk.py +280 -0
- interbolt-0.2.0/src/interbolt/utils/__init__.py +20 -0
- interbolt-0.1.1/src/interbolt/utils/__init__.py → interbolt-0.2.0/src/interbolt/utils/context.py +2 -33
- interbolt-0.2.0/src/interbolt/utils/log.py +21 -0
- interbolt-0.2.0/src/interbolt/utils/names.py +110 -0
- interbolt-0.2.0/src/interbolt/utils/signatures.py +20 -0
- interbolt-0.2.0/tests/architecture/test_layering.py +53 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/conftest.py +7 -7
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/test_agent_loop.py +64 -1
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/test_audit.py +12 -11
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/test_concurrency.py +61 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/test_endorse.py +2 -1
- interbolt-0.2.0/tests/integration/test_serialization.py +190 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/policies/agent_loop.yaml +2 -3
- interbolt-0.2.0/tests/unit/__init__.py +0 -0
- interbolt-0.2.0/tests/unit/test_cli.py +582 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_constants.py +1 -1
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_enforcement.py +166 -39
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_models.py +45 -11
- interbolt-0.2.0/tests/unit/test_policy_engine.py +1003 -0
- interbolt-0.2.0/tests/unit/test_policy_explain.py +367 -0
- interbolt-0.2.0/tests/unit/test_policy_schema.py +1188 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_reporting.py +20 -24
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_reporting_otel.py +35 -13
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_runtime.py +273 -16
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_runtime_guard.py +95 -103
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_taint.py +184 -34
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_taint_stress.py +1 -1
- interbolt-0.2.0/tests/unit/test_taint_wire.py +702 -0
- interbolt-0.1.1/CHANGELOG.md +0 -37
- interbolt-0.1.1/CLAUDE.md +0 -78
- interbolt-0.1.1/PKG-INFO +0 -189
- interbolt-0.1.1/README.md +0 -157
- interbolt-0.1.1/docs/concepts/identity.md +0 -169
- interbolt-0.1.1/docs/concepts/namespacing.md +0 -50
- interbolt-0.1.1/docs/concepts/policies.md +0 -287
- interbolt-0.1.1/docs/concepts/taint-propagation.md +0 -279
- interbolt-0.1.1/docs/design/deferred.md +0 -129
- interbolt-0.1.1/docs/guides/auditing.md +0 -158
- interbolt-0.1.1/docs/guides/ci.md +0 -59
- interbolt-0.1.1/docs/guides/mcp.md +0 -54
- interbolt-0.1.1/docs/guides/otel.md +0 -41
- interbolt-0.1.1/docs/guides/testing.md +0 -136
- interbolt-0.1.1/docs/index.md +0 -85
- interbolt-0.1.1/docs/quickstart.md +0 -261
- interbolt-0.1.1/docs/reference/api.md +0 -438
- interbolt-0.1.1/docs/reference/events.md +0 -172
- interbolt-0.1.1/docs/reference/performance.md +0 -72
- interbolt-0.1.1/docs/reference/reporters.md +0 -256
- interbolt-0.1.1/src/interbolt/enforcement/__init__.py +0 -435
- interbolt-0.1.1/src/interbolt/models/__init__.py +0 -1
- interbolt-0.1.1/src/interbolt/policy/schema.py +0 -202
- interbolt-0.1.1/src/interbolt/policy.example.yaml +0 -64
- interbolt-0.1.1/src/interbolt/runtime/__init__.py +0 -470
- interbolt-0.1.1/src/interbolt/runtime/guard.py +0 -189
- interbolt-0.1.1/src/interbolt/taint/__init__.py +0 -417
- interbolt-0.1.1/src/interbolt/taint/endorse.py +0 -225
- interbolt-0.1.1/tests/unit/test_cli.py +0 -313
- interbolt-0.1.1/tests/unit/test_policy_engine.py +0 -551
- interbolt-0.1.1/tests/unit/test_policy_schema.py +0 -319
- {interbolt-0.1.1 → interbolt-0.2.0}/.github/workflows/ci.yml +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/.pre-commit-config.yaml +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/.python-version +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/LICENSE +0 -0
- /interbolt-0.1.1/tests/unit/__init__.py → /interbolt-0.2.0/src/interbolt/py.typed +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/fixtures/__init__.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/fixtures/agents.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/fixtures/model.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/fixtures/sources.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/fixtures/tools.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/integration/test_multi_module_agent.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_errors.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/tests/unit/test_utils.py +0 -0
- {interbolt-0.1.1 → interbolt-0.2.0}/uv.lock +0 -0
|
@@ -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:
|
|
75
|
-
echo "Then commit and push
|
|
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
|
|
@@ -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.
|