Repo polish v2 - #161
Conversation
…ne, warning preservation, SPDX migration
Round 8 — exception de-duplication (policy.py): AirlockConfigurationError extends
ValueError, so clean boot-time validation errors raised inside _parse_rule were being
caught by the broad except (KeyError, TypeError, ValueError) handler in
PolicyEngine.__init__ and silently re-wrapped with a redundant "Invalid rule definition"
prefix. An explicit except AirlockConfigurationError: raise guard inserted before the
broad handler lets those errors surface natively.
Round 8 — pre-sieve warning preservation (exception.py, boundary.py):
AirlockPolicyViolation.__init__ now accepts optional warnings: list[str] | None (stored
as self.warnings). When AirlockBoundary.process() raises AirlockPolicyViolation on a
post-sieve deny verdict, it passes verdict.warnings so callers can inspect accumulated
pre-sieve warn-action diagnostic context via exc.warnings without losing it at the
exception boundary. RuntimeError message contract is unchanged; all existing callers
remain compatible.
Release prep — SPDX license metadata (pyproject.toml): project.license migrated from
the deprecated TOML table form ({ text = "MIT" }) to the SPDX string literal ("MIT").
Deprecated License :: OSI Approved :: MIT License classifier removed. Eliminates two
SetuptoolsDeprecationWarning emissions that become hard build errors after 2027-02-18.
PEP 517 build validated: uv build --package sovereign-sdk-airlock produces clean sdist
and pure-Python wheel with no editable-path leakage and zero deprecation warnings.
Wheel METADATA confirmed: Name, Version, Requires-Python, Description-Content-Type,
all Requires-Dist entries, and full README.md long-description (3,401 bytes, UTF-8).
Documentation: CHANGELOG.md [Unreleased] promoted to [1.4.0] — 2026-07-05. ROADMAP.md
Phase 9.6 delivered list updated. Package and workspace README.md updated with
AirlockPolicyViolation.warnings inspection pattern. 84 tests pass, 0 regressions.
…tion Pre-sieve deny path now passes verdict.warnings to AirlockPolicyViolation, closing the diagnostic asymmetry with the post-sieve deny path (which already forwarded warnings in Round 8). Both deny paths now carry accumulated warn-rule messages on exc.warnings for caller inspection, completing the uniform diagnostic contract across the full AirlockBoundary.process() lifecycle. New test test_pre_sieve_deny_preserves_accumulated_warnings (TestAirlockBoundaryPolicyDenial): a payload containing both the guard_internal_namespaces warn pattern and the block_private_keys deny pattern triggers both rules pre-sieve; asserts that exc.warnings is non-empty and contains the guard_internal_namespaces message. Test delta: 84 → 85 cases. 85 passed, 0 failed, 0 regressions.
Greptile SummaryThis PR adds a
Confidence Score: 4/5Safe to merge with the understanding that post-sieve deny exceptions will carry incomplete warning context until the one-line fix is applied. The post-sieve deny path in packages/sovereign-airlock/src/sovereign_airlock/boundary.py — the post-sieve raise path at line 151. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant AirlockBoundary
participant PolicyEngine
participant Sieve
Caller->>AirlockBoundary: process(payload)
AirlockBoundary->>PolicyEngine: evaluate(payload)
PolicyEngine-->>AirlockBoundary: verdict (allowed/denied, warnings[])
alt pre-sieve deny
AirlockBoundary-->>Caller: "raise AirlockPolicyViolation(violations, warnings=verdict.warnings)"
else allowed
AirlockBoundary->>Sieve: sieve_with_metrics(content)
Sieve-->>AirlockBoundary: sieve_output + telemetry
AirlockBoundary->>PolicyEngine: evaluate_post_sieve(telemetry)
PolicyEngine-->>AirlockBoundary: post_verdict (allowed/denied, post_warnings[])
alt post-sieve deny
Note over AirlockBoundary: post_verdict.warnings dropped
AirlockBoundary-->>Caller: "raise AirlockPolicyViolation(violations, warnings=verdict.warnings only)"
else allowed
AirlockBoundary->>AirlockBoundary: verdict.warnings.extend(post_verdict.warnings)
AirlockBoundary-->>Caller: AirlockResult(sieved_content, telemetry, receipt, policy_warnings)
end
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Caller
participant AirlockBoundary
participant PolicyEngine
participant Sieve
Caller->>AirlockBoundary: process(payload)
AirlockBoundary->>PolicyEngine: evaluate(payload)
PolicyEngine-->>AirlockBoundary: verdict (allowed/denied, warnings[])
alt pre-sieve deny
AirlockBoundary-->>Caller: "raise AirlockPolicyViolation(violations, warnings=verdict.warnings)"
else allowed
AirlockBoundary->>Sieve: sieve_with_metrics(content)
Sieve-->>AirlockBoundary: sieve_output + telemetry
AirlockBoundary->>PolicyEngine: evaluate_post_sieve(telemetry)
PolicyEngine-->>AirlockBoundary: post_verdict (allowed/denied, post_warnings[])
alt post-sieve deny
Note over AirlockBoundary: post_verdict.warnings dropped
AirlockBoundary-->>Caller: "raise AirlockPolicyViolation(violations, warnings=verdict.warnings only)"
else allowed
AirlockBoundary->>AirlockBoundary: verdict.warnings.extend(post_verdict.warnings)
AirlockBoundary-->>Caller: AirlockResult(sieved_content, telemetry, receipt, policy_warnings)
end
end
Reviews (1): Last reviewed commit: "fix(airlock): close pre-sieve warning as..." | Re-trigger Greptile |
| post_verdict = self._policy.evaluate_post_sieve(telemetry) | ||
| if not post_verdict.allowed: | ||
| raise AirlockPolicyViolation("; ".join(post_verdict.violations)) | ||
| raise AirlockPolicyViolation( | ||
| "; ".join(post_verdict.violations), | ||
| warnings=verdict.warnings, | ||
| ) |
There was a problem hiding this comment.
Post-sieve deny drops concurrent post-sieve warnings. When a post-sieve deny fires, the code passes only
verdict.warnings (pre-sieve) to the exception, silently discarding any post_verdict.warnings accumulated during the same evaluate_post_sieve call. This includes prose-tax threshold warnings and any warn-action telemetry rules that fired alongside the deny. A caller inspecting exc.warnings for diagnostics will be missing half the context in this scenario.
| post_verdict = self._policy.evaluate_post_sieve(telemetry) | |
| if not post_verdict.allowed: | |
| raise AirlockPolicyViolation("; ".join(post_verdict.violations)) | |
| raise AirlockPolicyViolation( | |
| "; ".join(post_verdict.violations), | |
| warnings=verdict.warnings, | |
| ) | |
| post_verdict = self._policy.evaluate_post_sieve(telemetry) | |
| if not post_verdict.allowed: | |
| raise AirlockPolicyViolation( | |
| "; ".join(post_verdict.violations), | |
| warnings=verdict.warnings + post_verdict.warnings, | |
| ) |
No description provided.