Skip to content

Airlock v3 - #155

Closed
kenwalger wants to merge 3 commits into
mainfrom
airlock-v3
Closed

Airlock v3#155
kenwalger wants to merge 3 commits into
mainfrom
airlock-v3

Conversation

@kenwalger

Copy link
Copy Markdown
Owner

No description provided.

kenwalger added 3 commits July 5, 2026 19:54
…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-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces sovereign-sdk-airlock (v1.4.0), a new monorepo workspace member implementing a model-neutral outbound governance boundary. The package enforces YAML-configured policies, runs payload through a sieve convergence pass, and produces a signed ForensicReceipt committed to the sovereign ledger.

  • Five new modules (boundary.py, payload.py, policy.py, receipt.py, telemetry.py) implement the four-component Airlock lifecycle \u2014 policy evaluation, sieve convergence, post-sieve telemetry evaluation, and evidence generation \u2014 behind a single async AirlockBoundary.process() entry point.
  • 73-case test suite across four test files covers happy-path lifecycle, deny/warn/allow policy actions, transport normalisation (OpenAI, Anthropic, raw), deep payload immutability, ledger resiliency, and post-sieve telemetry rules.

Confidence Score: 4/5

The 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 metric: raw_token is accepted without error and then silently falls back to token_estimate during pre-sieve evaluation rather than raising AirlockConfigurationError, meaning governance rules can appear configured while quietly checking the wrong value. All other modules are well-structured, immutably designed, and well-tested.

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

Filename Overview
packages/sovereign-airlock/src/sovereign_airlock/boundary.py Async orchestrator for the four-component Airlock lifecycle; pre/post-sieve policy evaluation, non-fatal evidence recording, and clean AirlockResult assembly are all correctly implemented.
packages/sovereign-airlock/src/sovereign_airlock/policy.py PolicyEngine loads and evaluates YAML rules with good early-fail validation for scope and action, but the metric field for telemetry-scoped rules is not validated at init time — a typo silently proxies against token_estimate. The prose_tax_warning_threshold fractional-vs-percentage unit convention is also easy to misconfigure.
packages/sovereign-airlock/src/sovereign_airlock/telemetry.py Frozen AirlockTelemetry with zero-token guard and negative-savings clamp; upper-bound clamp to 100.0 is documented but not enforced.
packages/sovereign-airlock/src/sovereign_airlock/payload.py NormalizedPayload frozen dataclass with deep immutability (tuple + MappingProxyType); three factory functions for OpenAI, Anthropic, and raw normalisation are well-implemented.
packages/sovereign-airlock/src/sovereign_airlock/receipt.py ReceiptBuilder correctly separates signing (fatal) from ledger commit (non-fatal) per SAR-0009; metadata assembly is accurate and matches test assertions.
packages/sovereign-airlock/tests/test_boundary.py Comprehensive 25-case async test suite covering the full lifecycle, all three transports, resiliency scenarios, prose-tax threshold, and payload immutability; asyncio_mode=auto in root pyproject.toml covers async test collection.
packages/sovereign-airlock/pyproject.toml New workspace member at v1.4.0 with correct runtime dependencies (core, ledger, sieve, pyyaml); workspace registration in root pyproject.toml and uv.lock is consistent.

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)
Loading
%%{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)
Loading

Comments Outside Diff (3)

  1. packages/sovereign-airlock/src/sovereign_airlock/policy.py, line 1008-1054 (link)

    P1 Missing metric name validation for telemetry-scoped rules

    _parse_rule validates scope against _VALID_SCOPES and action against _VALID_ACTIONS at init time, raising AirlockConfigurationError on unknown values — but it does not validate the metric field for telemetry-scoped rules. A typo such as metric: raw_token (missing the trailing s) is silently accepted.

    At evaluation time the inconsistency becomes a governance defect: in _evaluate_telemetry, when telemetry=None, any metric name that is not in _POST_SIEVE_METRICS falls back to metric_value = payload.token_estimate (the raw_tokens fallback path), so the rule silently evaluates the wrong value instead of failing. A _VALID_METRICS: frozenset[str] = frozenset({"raw_tokens", "sieved_tokens", "tax_savings_percentage"}) check in _parse_rule (for scope == "telemetry") would catch this at the same time as malformed regex patterns — providing the same early-fail guarantee the rest of the config loading establishes.

  2. packages/sovereign-airlock/src/sovereign_airlock/telemetry.py, line 1449-1454 (link)

    P2 Upper bound of tax_savings_percentage not enforced despite documented [0.0, 100.0] clamp

    The docstring states the field is "clamped to [0.0, 100.0]", but only the lower bound is enforced via max(0.0, ...). While token counts from sovereign_sieve cannot go negative today, if sieve_output.optimized_token_count were ever negative (an upstream contract change or bug), the calculated percentage would exceed 100% with no guard. Adding min(100.0, max(0.0, ...)) would match the stated invariant and future-proof the clamp.

  3. packages/sovereign-airlock/src/sovereign_airlock/policy.py, line 1137-1143 (link)

    P2 prose_tax_warning_threshold unit convention is implicit and inconsistent with max_token_ceiling

    _prose_tax_warning_threshold is stored as a raw decimal fraction (e.g., 0.35) and converted to percentage at comparison time via * 100.0. The peer config field max_token_ceiling uses direct integer counts. A user who reads the YAML side-by-side might configure prose_tax_warning_threshold: 35 intending 35%, which would produce threshold_pct = 3500.0 and permanently trigger a warning on every payload. The README example of 0.35 demonstrates the correct form but does not call out the fractional-vs-direct distinction. Adding a docstring note or an upper-bound validation (if value > 1.0: raise AirlockConfigurationError(...)) would prevent silent misconfiguration.

Reviews (1): Last reviewed commit: "fix(airlock): resolve three Round 3 PR r..." | Re-trigger Greptile

@kenwalger kenwalger closed this Jul 6, 2026
@kenwalger
kenwalger deleted the airlock-v3 branch July 6, 2026 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant