Skip to content

Airlock v4 - #156

Closed
kenwalger wants to merge 4 commits into
mainfrom
airlock-v4
Closed

Airlock v4#156
kenwalger wants to merge 4 commits into
mainfrom
airlock-v4

Conversation

@kenwalger

Copy link
Copy Markdown
Owner

No description provided.

kenwalger added 4 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.
…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-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces sovereign-sdk-airlock, a new workspace package implementing a model-neutral outbound governance boundary that inspects, policy-evaluates, minimises, and cryptographically records structured payloads before they cross a sovereign perimeter (SAR-0004).

  • Core modules added: AirlockBoundary (async 4-component orchestrator), PolicyEngine (offline YAML rule evaluator with raw/fields/telemetry scopes), AirlockTelemetry (frozen sieve metrics with ZeroDivisionError guard and full savings clamp), ReceiptBuilder (Ed25519 signing + non-fatal ledger commit), NormalizedPayload (deep-immutable provider-neutral inspection surface with OpenAI/Anthropic/raw factory functions).
  • 76-case test suite across four files covering all three rule scopes and actions, boot-time config validation (regex, metric name, threshold range), post-sieve evaluation, deep immutability, transport neutrality, and resiliency; documentation, CHANGELOG, and workspace plumbing (pyproject.toml, uv.lock) updated accordingly.

Confidence Score: 4/5

Safe to merge with the understanding that raw_tokens deny/warn rules evaluate a UTF-8 byte heuristic rather than the actual tokenizer count from the sieve library.

The governance boundary's raw_tokens telemetry rules are evaluated pre-sieve against payload.token_estimate (bytes ÷ 4) and never re-checked against sieve_output.raw_token_count post-sieve. For ASCII-heavy English prose the two values are close, but code-heavy payloads or multi-byte Unicode can diverge enough that a deny rule on raw_tokens silently fails to fire even when the actual token count exceeds the configured threshold. All other enforcement paths — fields, raw regex, sieved_tokens, tax_savings_percentage, the global ceiling — are correctly gated against their actual measured values.

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

Filename Overview
packages/sovereign-airlock/src/sovereign_airlock/boundary.py Async orchestrator for the 4-component Airlock lifecycle. Pre-sieve raw_tokens evaluation uses heuristic token_estimate rather than the actual sieve raw_token_count, creating a governance enforcement gap when the estimate diverges from the actual count.
packages/sovereign-airlock/src/sovereign_airlock/policy.py YAML policy engine with boot-time regex compilation, metric name validation, and prose_tax_warning_threshold range enforcement. _POST_SIEVE_METRICS gate correctly prevents pre-sieve proxy of sieved_tokens/tax_savings_percentage rules.
packages/sovereign-airlock/src/sovereign_airlock/telemetry.py Frozen AirlockTelemetry dataclass with ZeroDivisionError guard and full [0.0, 100.0] clamp on tax_savings_percentage. SHA-256 payload hash correctly derived from raw pre-sieve content.
packages/sovereign-airlock/src/sovereign_airlock/receipt.py ReceiptBuilder assembles and commits ForensicReceipts via SovereignKeyManager. Ledger write failures are non-fatal per SAR-0009; signing exceptions propagate to the caller.
packages/sovereign-airlock/src/sovereign_airlock/payload.py NormalizedPayload frozen dataclass with deep immutability via tuple and MappingProxyType conversion in __post_init__. Factory functions for OpenAI, Anthropic, and raw transport normalisation.
packages/sovereign-airlock/tests/test_boundary.py 25-case integration suite covering happy path, policy denial, post-sieve denial, warnings, transport neutrality, and resiliency.
packages/sovereign-airlock/tests/test_telemetry.py 12-case telemetry unit tests including ZeroDivisionError guard, clamp edge cases, and SHA-256 correctness. Live sieve round-trip test computes expected_pct without the [0.0, 100.0] clamp applied by from_sieve_output.
packages/sovereign-airlock/tests/test_policy.py 29-case policy engine tests covering all three evaluation scopes, all three actions, global ceiling enforcement, boot-time validation, post-sieve evaluation, and field tuple immutability.
packages/sovereign-airlock/tests/test_receipts.py 10-case receipt tests covering metadata invariants, cryptographic verifiability, ledger commit, non-fatal ledger failure, and no-ledger operation.
packages/sovereign-airlock/pyproject.toml New workspace member at version 1.4.0 with correct runtime dependencies on sovereign-sdk-core, sovereign-sdk-ledger, sovereign-sdk-sieve, and pyyaml>=6.0.

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

Comments Outside Diff (1)

  1. packages/sovereign-airlock/src/sovereign_airlock/boundary.py, line 619-634 (link)

    P1 raw_tokens deny rules silently bypass when estimate diverges from actual count

    The pre-sieve evaluate(payload) call at line 620 passes no telemetry, so _evaluate_telemetry falls back to payload.token_estimate (UTF-8 bytes ÷ 4 heuristic) for raw_tokens rules. After the sieve pass, evaluate_post_sieve(telemetry) only covers _POST_SIEVE_METRICS = {"sieved_tokens", "tax_savings_percentage"} and never re-evaluates raw_tokens against the actual telemetry.raw_tokens (= sieve_output.raw_token_count).

    For a deny rule at raw_tokens: 10000, if token_estimate = 9800 but sieve_output.raw_token_count = 13000 (which happens with code-heavy or multi-byte content where bytes ÷ 4 underestimates), the rule never fires and the oversized payload crosses the boundary unchecked. This is a live governance enforcement gap for any deployment relying on raw_tokens deny rules.

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

Comment on lines +182 to +197
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"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +87 to +97
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +294 to +295
assert result.telemetry.raw_tokens >= result.telemetry.sieved_tokens
assert 0.0 <= result.telemetry.tax_savings_percentage <= 100.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

@kenwalger kenwalger closed this Jul 6, 2026
@kenwalger kenwalger reopened this Jul 6, 2026
@kenwalger kenwalger closed this Jul 6, 2026
@kenwalger
kenwalger deleted the airlock-v4 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