Airlock v4 - #156
Conversation
…bling convention Renames `packages/sovereign-sdk-airlock/` to `packages/sovereign-airlock/` to match the established directory layout of sibling packages (`sovereign-core`, `sovereign-sieve`, `sovereign-ledger`, `sovereign-edge`, `sovereign-sensor`). Distribution name (`sovereign-sdk-airlock`), import namespace (`sovereign_airlock`), and all runtime dependency declarations are unchanged. Changes: - packages/sovereign-sdk-airlock/ → packages/sovereign-airlock/ (directory rename) - Updated file-path header comments in all 7 source modules under src/sovereign_airlock/ - uv.lock: editable path references updated from packages/sovereign-sdk-airlock to packages/sovereign-airlock; re-resolved cleanly (uv lock) - README.md: workspace topography tree entry corrected - CHANGELOG.md: workspace member path reference corrected - ROADMAP.md: Phase 9.6 pyproject.toml path reference corrected Verification: 469 passed, 1 skipped. Zero regressions.
…validation, prose tax threshold, payload immutability
Round 1 PR remediation pass for sovereign-sdk-airlock (v1.4.0).
Defect 1 — PolicyEngine regex boot-time validation (policy.py):
- PolicyRule.pattern type changed from str | None to re.Pattern[str] | None.
- _parse_rule() compiles all patterns via re.compile() at PolicyEngine.__init__
time; re.error is caught and re-raised as AirlockConfigurationError immediately,
guaranteeing bad configurations fail at startup rather than at evaluation time.
- _evaluate_raw() and _evaluate_fields() updated to call rule.pattern.search()
on the pre-compiled Pattern object.
- New test: TestPolicyLoading.test_raises_on_malformed_regex_pattern.
Defect 2 — Prose Tax warning threshold lifecycle (policy.py, boundary.py):
- PolicyEngine.check_prose_tax_threshold(telemetry) added: returns a non-fatal
warning when tax_savings_percentage falls below prose_tax_warning_threshold * 100.
Threshold of 0.0 (default) disables the check.
- AirlockBoundary.process() calls check_prose_tax_threshold() after sieve
convergence; warnings are extended onto verdict.warnings before evidence
generation so they are sealed in receipt metadata.
- conftest.py: standard test policy prose_tax_warning_threshold reduced to 0.0
to decouple baseline fixture from threshold behaviour tests.
- New tests: TestProseTaxThreshold (2 cases).
Defect 3 — Deep immutability on NormalizedPayload (payload.py):
- from __future__ import annotations added; import types introduced with no
eager evaluation namespace leaks.
- __post_init__ converts: content → tuple[str, ...]; metadata →
types.MappingProxyType[str, Any]; each tools entry →
types.MappingProxyType[str, Any] stored in an outer tuple. object.__setattr__
used to bypass frozen=True. Constructor still accepts plain list/dict equivalents.
- default_factory updated to MappingProxyType({}) for metadata and tuple for tools.
- New tests: TestNormalizedPayloadImmutability (4 cases).
Test delta: 59 → 66 cases (+7). 476 passed, 1 skipped workspace-wide. Zero regressions.
Updates: CHANGELOG.md, ROADMAP.md.
…y metric fallback, PolicyRule immutability, negative savings clamp
Round 3 PR remediation pass for sovereign-sdk-airlock (v1.4.0).
**Defect 1 — Telemetry Metric Evaluation Fallback (policy.py + boundary.py):**
Introduced `_POST_SIEVE_METRICS: frozenset[str]` (`sieved_tokens`,
`tax_savings_percentage`) as a module-level guard in `_evaluate_telemetry`. When
`telemetry=None` pre-sieve, rules for these metrics are now skipped entirely rather
than incorrectly proxied against `payload.token_estimate`. Added
`PolicyEngine.evaluate_post_sieve(telemetry: AirlockTelemetry) -> PolicyVerdict`
which evaluates post-sieve-only telemetry rules and the prose tax threshold against
actual sieve output. Replaced `check_prose_tax_threshold()` call in
`AirlockBoundary.process()` with `evaluate_post_sieve()`; a `deny` post-sieve verdict
now raises `AirlockPolicyViolation`, preventing evidence generation for payloads that
exceed sieved-token hard caps.
**Defect 2 — PolicyRule.fields Immutability (policy.py):**
Changed `PolicyRule.fields` from `list[str]` to `tuple[str, ...]`. Updated
`_parse_rule()` to use `tuple(rule_def.get("fields") or ())`. Field iteration in
`_evaluate_fields` is unchanged; both `list` and `tuple` support the `for ... in`
protocol.
**Defect 3 — Clamp Negative Sieve Savings (telemetry.py):**
Wrapped the savings calculation in `from_sieve_output()` with `max(0.0, round(...))`.
When the sieve expands content (sieved tokens > raw tokens), the percentage is clamped
to `0.0` rather than producing a negative value that would incorrectly satisfy
`tax_savings_percentage > threshold` telemetry rules.
Test delta: 66 → 73 cases (+7). 483 passed, 1 skipped workspace-wide. Zero regressions.
…y metric validation, prose tax threshold range guard, telemetry savings full clamp
Round 4 (final) PR remediation pass for sovereign-sdk-airlock (v1.4.0).
**Defect 1 — Validate Telemetry Metric Names at Boot Time (policy.py):**
Added `_VALID_METRICS: frozenset[str] = frozenset({"raw_tokens", "sieved_tokens",
"tax_savings_percentage"})` at module level. In `_parse_rule`, when `scope == "telemetry"`
and `metric` is present, the value is verified against `_VALID_METRICS`; an unrecognised
name (e.g. the typo `"raw_token"`) raises `AirlockConfigurationError` immediately at
`PolicyEngine.__init__` time rather than silently producing a rule that never evaluates.
**Defect 2 — Enforce Strict Range Constraints on Prose Tax Threshold (policy.py):**
After parsing `prose_tax_warning_threshold` in `__init__`, the value is validated to be
within `[0.0, 1.0]`. An accidentally percentage-scaled value such as `35.0` raises
`AirlockConfigurationError` with the out-of-range value surfaced in the message, preventing
a silent misconfiguration where the threshold is never reachable.
**Defect 3 — Complete Telemetry Boundary Clamping (telemetry.py):**
Extended the existing `max(0.0, ...)` lower clamp (Round 3) to a full
`max(0.0, min(100.0, round(...)))`, enforcing the documented `[0.0, 100.0]` range.
An impossible inversion (e.g. negative `optimized_token_count` producing > 100% savings)
is now clamped to `100.0` rather than producing a value that exceeds the documented ceiling.
Test delta: 73 → 76 cases (+3). 486 passed, 1 skipped workspace-wide. Zero regressions.
Greptile SummaryThis PR introduces
Confidence Score: 4/5Safe to merge with the understanding that The governance boundary's packages/sovereign-airlock/src/sovereign_airlock/boundary.py — the pre-sieve evaluate call and the post-sieve evaluate_post_sieve call together leave raw_tokens rules without a post-sieve correction step. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant C as Caller
participant AB as AirlockBoundary
participant PE as PolicyEngine
participant SS as sovereign_sieve
participant RB as ReceiptBuilder
participant L as SovereignLedger
C->>AB: process(NormalizedPayload)
AB->>PE: "evaluate(payload, telemetry=None)"
Note over PE: raw + fields + raw_tokens rules (raw_tokens uses token_estimate heuristic)
PE-->>AB: PolicyVerdict
alt "verdict.allowed == False"
AB-->>C: raise AirlockPolicyViolation
end
AB->>SS: sieve_with_metrics(raw_content)
SS-->>AB: SieveOutput
AB->>AB: AirlockTelemetry.from_sieve_output(sieve_output, raw_content)
AB->>PE: evaluate_post_sieve(telemetry)
Note over PE: sieved_tokens + tax_savings_percentage rules + prose_tax_warning_threshold check
PE-->>AB: PolicyVerdict (post-sieve)
alt "post_verdict.allowed == False"
AB-->>C: raise AirlockPolicyViolation
end
AB->>RB: build_and_commit(sieved_content, telemetry, warnings, source)
RB->>RB: assemble metadata + generate_receipt()
RB->>L: append_receipt(receipt, sieved_content)
alt Ledger write fails
L-->>RB: Exception
RB->>RB: log WARNING (non-fatal)
end
RB-->>AB: ForensicReceipt
AB-->>C: AirlockResult(sieved_content, telemetry, receipt, policy_warnings)
%%{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 C as Caller
participant AB as AirlockBoundary
participant PE as PolicyEngine
participant SS as sovereign_sieve
participant RB as ReceiptBuilder
participant L as SovereignLedger
C->>AB: process(NormalizedPayload)
AB->>PE: "evaluate(payload, telemetry=None)"
Note over PE: raw + fields + raw_tokens rules (raw_tokens uses token_estimate heuristic)
PE-->>AB: PolicyVerdict
alt "verdict.allowed == False"
AB-->>C: raise AirlockPolicyViolation
end
AB->>SS: sieve_with_metrics(raw_content)
SS-->>AB: SieveOutput
AB->>AB: AirlockTelemetry.from_sieve_output(sieve_output, raw_content)
AB->>PE: evaluate_post_sieve(telemetry)
Note over PE: sieved_tokens + tax_savings_percentage rules + prose_tax_warning_threshold check
PE-->>AB: PolicyVerdict (post-sieve)
alt "post_verdict.allowed == False"
AB-->>C: raise AirlockPolicyViolation
end
AB->>RB: build_and_commit(sieved_content, telemetry, warnings, source)
RB->>RB: assemble metadata + generate_receipt()
RB->>L: append_receipt(receipt, sieved_content)
alt Ledger write fails
L-->>RB: Exception
RB->>RB: log WARNING (non-fatal)
end
RB-->>AB: ForensicReceipt
AB-->>C: AirlockResult(sieved_content, telemetry, receipt, policy_warnings)
|
| raw_metric: str | None = rule_def.get("metric") | ||
| if scope == "telemetry" and raw_metric is not None and raw_metric not in _VALID_METRICS: | ||
| raise AirlockConfigurationError( | ||
| f"Unknown telemetry metric '{raw_metric}' in rule '{name}'; " | ||
| f"expected one of {sorted(_VALID_METRICS)}." | ||
| ) | ||
|
|
||
| return PolicyRule( | ||
| name=name, | ||
| scope=scope, | ||
| action=action, | ||
| pattern=compiled, | ||
| fields=tuple(rule_def.get("fields") or ()), | ||
| metric=raw_metric, | ||
| threshold=rule_def.get("threshold"), | ||
| ) |
There was a problem hiding this comment.
Missing required-field validation for scope-specific rule fields
_parse_rule validates metric names and regex patterns but does not enforce that scope-specific required fields are actually present. A telemetry-scope rule with no metric key produces PolicyRule(metric=None, threshold=None) which silently returns early in _evaluate_telemetry (if rule.metric is None or rule.threshold is None: return) and is also skipped in evaluate_post_sieve (rule.metric not in _POST_SIEVE_METRICS is True when metric is None). Likewise, a fields-scope rule without a fields key and a raw-scope rule without a pattern both become silent no-ops via the same guard pattern in their evaluators. In all cases a misconfigured deny rule is accepted at init time and never fires — the system silently fails to enforce governance. Validation guards like if scope == "telemetry" and not raw_metric: raise AirlockConfigurationError(...) are missing for each scope.
| metadata: dict[str, Any] = { | ||
| "boundary": "sovereign-sdk-airlock", | ||
| "source_transport": source, | ||
| "prose_tax_summary": { | ||
| "raw_token_count": telemetry.raw_tokens, | ||
| "optimized_token_count": telemetry.sieved_tokens, | ||
| "tax_savings_percentage": telemetry.tax_savings_percentage, | ||
| }, | ||
| } | ||
| if policy_warnings: | ||
| metadata["policy_warnings"] = policy_warnings |
There was a problem hiding this comment.
Raw-content hash absent from signed receipt metadata
The receipt's signed metadata includes prose_tax_summary (token counts and savings) but omits telemetry.payload_hash, the SHA-256 digest of the original pre-sieve content. The receipt cryptographically binds to sieved_content via generate_receipt({"content": sieved_content}, metadata), but without embedding the raw hash in metadata, there is no way to verify from the receipt alone what the original input was. Adding "raw_payload_hash": telemetry.payload_hash to metadata would close this forensic gap.
| assert result.telemetry.raw_tokens >= result.telemetry.sieved_tokens | ||
| assert 0.0 <= result.telemetry.tax_savings_percentage <= 100.0 |
There was a problem hiding this comment.
Fragile assertion assumes sieve never expands content
assert result.telemetry.raw_tokens >= result.telemetry.sieved_tokens will fail if sovereign_sieve.sieve_with_metrics returns optimized_token_count > raw_token_count — a scenario the telemetry layer explicitly handles by clamping tax_savings_percentage to 0.0 (see test_negative_savings_clamped_to_zero). Changing the assertion to assert 0.0 <= result.telemetry.tax_savings_percentage <= 100.0 matches what the spec actually promises.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
No description provided.