Airlock v3 - #155
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.
Greptile SummaryThis PR introduces
Confidence Score: 4/5The core orchestration, signing, ledger resiliency, and transport normalisation are solid. The one real concern is in policy.py’s rule parser — a mistyped metric name passes silently through init and causes the rule to evaluate the wrong data at runtime instead of rejecting the config early. The policy engine validates scope and action at init time but omits the same validation for the metric field on telemetry rules. A typo like packages/sovereign-airlock/src/sovereign_airlock/policy.py — specifically the _parse_rule method and its handling of the metric field for telemetry-scoped rules. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant AirlockBoundary
participant PolicyEngine
participant sieve_with_metrics
participant AirlockTelemetry
participant ReceiptBuilder
participant SovereignKeyManager
participant SovereignLedger
Caller->>AirlockBoundary: process(NormalizedPayload)
AirlockBoundary->>PolicyEngine: evaluate(payload)
PolicyEngine-->>AirlockBoundary: PolicyVerdict
alt deny verdict
AirlockBoundary-->>Caller: raise AirlockPolicyViolation
end
AirlockBoundary->>sieve_with_metrics: sieve_with_metrics(raw_content)
sieve_with_metrics-->>AirlockBoundary: SieveOutput
AirlockBoundary->>AirlockTelemetry: from_sieve_output(sieve_output, raw_content)
AirlockTelemetry-->>AirlockBoundary: AirlockTelemetry (frozen)
AirlockBoundary->>PolicyEngine: evaluate_post_sieve(telemetry)
PolicyEngine-->>AirlockBoundary: post PolicyVerdict
alt post-sieve deny verdict
AirlockBoundary-->>Caller: raise AirlockPolicyViolation
end
AirlockBoundary->>ReceiptBuilder: build_and_commit(sieved_content, telemetry, warnings, source)
ReceiptBuilder->>SovereignKeyManager: generate_receipt(payload_dict, metadata)
SovereignKeyManager-->>ReceiptBuilder: ForensicReceipt
ReceiptBuilder->>SovereignLedger: append_receipt(receipt, sieved_content)
Note over ReceiptBuilder,SovereignLedger: Ledger failure → WARNING log, non-fatal
SovereignLedger-->>ReceiptBuilder: ok
ReceiptBuilder-->>AirlockBoundary: ForensicReceipt
AirlockBoundary-->>Caller: AirlockResult(sieved_content, telemetry, receipt, 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 Caller
participant AirlockBoundary
participant PolicyEngine
participant sieve_with_metrics
participant AirlockTelemetry
participant ReceiptBuilder
participant SovereignKeyManager
participant SovereignLedger
Caller->>AirlockBoundary: process(NormalizedPayload)
AirlockBoundary->>PolicyEngine: evaluate(payload)
PolicyEngine-->>AirlockBoundary: PolicyVerdict
alt deny verdict
AirlockBoundary-->>Caller: raise AirlockPolicyViolation
end
AirlockBoundary->>sieve_with_metrics: sieve_with_metrics(raw_content)
sieve_with_metrics-->>AirlockBoundary: SieveOutput
AirlockBoundary->>AirlockTelemetry: from_sieve_output(sieve_output, raw_content)
AirlockTelemetry-->>AirlockBoundary: AirlockTelemetry (frozen)
AirlockBoundary->>PolicyEngine: evaluate_post_sieve(telemetry)
PolicyEngine-->>AirlockBoundary: post PolicyVerdict
alt post-sieve deny verdict
AirlockBoundary-->>Caller: raise AirlockPolicyViolation
end
AirlockBoundary->>ReceiptBuilder: build_and_commit(sieved_content, telemetry, warnings, source)
ReceiptBuilder->>SovereignKeyManager: generate_receipt(payload_dict, metadata)
SovereignKeyManager-->>ReceiptBuilder: ForensicReceipt
ReceiptBuilder->>SovereignLedger: append_receipt(receipt, sieved_content)
Note over ReceiptBuilder,SovereignLedger: Ledger failure → WARNING log, non-fatal
SovereignLedger-->>ReceiptBuilder: ok
ReceiptBuilder-->>AirlockBoundary: ForensicReceipt
AirlockBoundary-->>Caller: AirlockResult(sieved_content, telemetry, receipt, warnings)
|
No description provided.