Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/spark-proof-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# SPDX-License-Identifier: PMPL-1.0
name: SPARK Proof Gate

# ambientops anti-theatre gate (P0 of the estate Rust/SPARK audit;
# hyperpolymath/standards#124 / #135).
#
# Self-contained on purpose: it does not `uses:` the standards reusable
# SPARK Theatre Gate yet, so it works the moment this lands rather than
# waiting on hyperpolymath/standards#141 to merge. Once #141 is on
# standards@main this file SHOULD be reduced to a 3-line caller:
#
# jobs:
# gate:
# uses: hyperpolymath/standards/.github/workflows/spark-theatre-gate.yml@<sha>
# with: { paths: "total-update/ada", enforce_zero_contract: false }
#
# It exists specifically so the safety_boundary / reversibility_types /
# safety_invariant honest-demotion cannot silently regress to a false
# "formally verified" banner. T1 is a hard failure; T2 (zero-contract
# SPARK_Mode) is a warning while the Ada/SPARK->Rust/SPARK migration tail
# clears (tracked in PROOF-NEEDS.md).

on:
push:
branches: [main, master]
paths: ["total-update/ada/**", ".github/workflows/spark-proof-gate.yml"]
pull_request:
paths: ["total-update/ada/**", ".github/workflows/spark-proof-gate.yml"]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
spark-proof-gate:
name: SPARK Proof Gate (dnfinition)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4

- name: Scan total-update/ada for SPARK proof theatre
run: |
set -uo pipefail
SCAN_ROOT="total-update/ada"

prover_evidence=0
if grep -rIlE 'gnatprove' .github/workflows 2>/dev/null | grep -q .; then
prover_evidence=1
fi
for bf in Justfile justfile Mustfile Dustfile; do
[ -f "$bf" ] && grep -qI 'gnatprove' "$bf" && prover_evidence=1
done
if find . \( -name 'gnatprove.out' -o -name '*.spark' \
-o -path '*/gnatprove/*.json' \) \
-not -path '*/.git/*' 2>/dev/null | grep -q .; then
prover_evidence=1
fi
echo "prover_evidence=$prover_evidence"

claim_re='formally verified|SPARK Proof Level|\(Gold\)|all preconditions and postconditions are verified|proving the invariant holds'
contract_re='Pre[[:space:]]*=>|Post[[:space:]]*=>|Global[[:space:]]*=>|Depends[[:space:]]*=>|Contract_Cases'
sparkon_re='pragma[[:space:]]+SPARK_Mode[[:space:]]*\([[:space:]]*On[[:space:]]*\)|with[[:space:]]+SPARK_Mode[[:space:]]*=>[[:space:]]*On'

fail=0
while IFS= read -r f; do
[ -z "$f" ] && continue
grep -vE '^[[:space:]]*--' "$f" | grep -qE "$sparkon_re" || continue
header="$(awk '
/^[[:space:]]*--/ || /^[[:space:]]*$/ { print; next }
{ exit }' "$f")"
has_header_claim=0
printf '%s\n' "$header" | grep -qiE "$claim_re" && has_header_claim=1
has_contract=0
grep -qE "$contract_re" "$f" && has_contract=1

[ "$prover_evidence" -eq 1 ] && continue

if [ "$has_header_claim" -eq 1 ]; then
echo "::error file=$f::T1 SPARK theatre regression: header banner claims formal proof but no GNATprove run exists. Demote (SPARK_Mode Off + tracked obligation in PROOF-NEEDS.md) or wire gnatprove into CI."
fail=1
elif [ "$has_contract" -eq 0 ]; then
echo "::warning file=$f::T2 zero-contract SPARK_Mode (standards#135): proves nothing here. Demote to SPARK_Mode Off or add contracts."
fi
done < <(find "$SCAN_ROOT" \( -name '*.ads' -o -name '*.adb' \) \
-not -path '*/.git/*' -not -path '*/obj/*' 2>/dev/null)

if [ "$fail" -ne 0 ]; then
echo "::error::SPARK Proof Gate FAILED — a demoted unit regressed to a hollow 'verified' claim. See PROOF-NEEDS.md (O-REV-*/O-SAFE-*)."
exit 1
fi
echo "✓ SPARK Proof Gate passed — no hollow proof claims in total-update/ada."
24 changes: 24 additions & 0 deletions PROOF-NEEDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@
- **Privilege escalation prevention**: Prove system operations respect the principle of least privilege (no operation escalates beyond its declared scope)
- **Rollback atomicity**: Prove that failed operations roll back completely (no partial state)

## Tracked unproven obligations — dnfinition reversibility subsystem

The following obligations are referenced by the `PROOF STATUS` / `HONESTY
NOTE` headers in `total-update/ada/dnfinition/src/{reversibility,safety}/*`.
They were previously expressed as SPARK ghost/lemma bodies stubbed to
`return True` / `null;` (proof theatre). SPARK_Mode is now `Off` on those
units and the obligations are recorded here as the authoritative,
**UNPROVEN** list (Idris2 is the intended model — see Recommended prover).
The SPARK Theatre Gate (`.github/workflows/spark-proof-gate.yml`) prevents
any of these units silently regressing to a false "verified" claim.

| ID | Unit | Obligation |
|----|------|------------|
| O-REV-1 | `reversibility_types.ads` | `Snapshot_Exists`: a snapshot referenced by a non-null `Snapshot_ID` is actually present in backend storage. |
| O-REV-2 | `reversibility_types.ads` | `Snapshot_Is_Valid`: a `Valid`-state snapshot is restorable. |
| O-REV-3 | `reversibility_types.ads` | `System_State_Matches_Snapshot`: after a `Completed` rollback, on-disk system state equals the snapshot's captured state (rollback atomicity / no partial state). Subsumes the "Rollback atomicity" item above. |
| O-REV-4 | `reversibility_types.ads` | Monotonic snapshot IDs: issued IDs strictly increase and are never reused after deletion. |
| O-SAFE-1 | `safety_invariant.ads` | The invariant "every modifying operation is preceded by a recovery point" (`Invariant_Holds`) is preserved by all state-update operations. |
| O-SAFE-2 | `safety_boundary.ads` | The token-typed API (`Recovery_Point_Token`) admits no path that performs a modifying `Safe_*` operation without a valid recovery point, i.e. the runtime-enforced invariant is also statically unviolable. |

Status: **none of O-REV-1..4 / O-SAFE-1..2 are checked at compile time or
runtime today.** They are honest debt, not regressions, and are
deliberately *not* claimed as proven anywhere in the source.

## Recommended prover
- **Idris2** — State machines and idempotency properties are natural fits for dependent types

Expand Down
1 change: 1 addition & 0 deletions total-update/ada/dnfinition/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
obj-check/
26 changes: 26 additions & 0 deletions total-update/ada/dnfinition/checkonly.gpr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- SPDX-License-Identifier: PMPL-1.0-or-later
-- Scoped compile-check project: verifies the reversibility/safety
-- subsystem (the units touched by the SPARK honest-demotion) WITHOUT
-- pulling the unrelated, unprovided `ncurses` TUI dependency. Not for
-- shipping — a verification harness only.

project Checkonly is

for Source_Dirs use
("src/reversibility",
"src/safety",
"src/backends",
"src/platform");
for Object_Dir use "obj-check";
-- No `for Main`: compile-only, no bind/link, no ncurses needed.

package Compiler is
for Switches ("Ada") use
("-gnat2022",
"-gnatwa",
"-gnato13",
"-gnatf",
"-gnata"); -- assertions on: exercises the retained Pre/Post
end Compiler;

end Checkonly;
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ package Backend_Interface is

type Transaction_Item is record
Kind : Transaction_Kind;
Package : Package_Info;
Pkg : Package_Info;
-- Renamed from `Package`: that is an Ada reserved word and was a
-- hard legality error (backend_interface.ads:71) that prevented the
-- dnfinition subsystem from compiling at all — the concrete reason
-- the old SPARK "formally verified" banners were pure theatre
-- (GNATprove can prove nothing about code that does not compile).
-- Field is unreferenced anywhere; rename is purely local.
end record;

type Transaction_List is array (Positive range <>) of Transaction_Item;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
-- SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell <jonathan@hyperpolymath.io>

-- Reversibility Types - Core types for transaction safety and rollback
-- This module uses SPARK for formal verification of reversibility guarantees

pragma SPARK_Mode (On);
--
-- HONESTY NOTE (2026-05-18 audit): This unit was previously marked
-- SPARK_Mode (On) and carried ghost functions
-- (Snapshot_Exists / System_State_Matches_Snapshot / ...) whose bodies
-- are stubbed to return True. No GNATprove proof has ever been run or
-- passed (the unit does not even pass SPARK Phase-2 legality: see the
-- record-component / type name clash on Rollback_Result.Snapshot_ID).
-- SPARK_Mode is therefore set Off until a genuine proof obligation
-- exists. The real reversibility obligations are tracked as proof debt
-- in PROOF-NEEDS.md (Idris2), NOT claimed as proven here.

pragma SPARK_Mode (Off);

with Ada.Calendar;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
Expand Down Expand Up @@ -87,33 +96,33 @@ package Reversibility_Types is
Requires_Reboot);

type Rollback_Result is record
Status : Rollback_Status := Not_Started;
Message : Unbounded_String;
Snapshot_ID : Snapshot_ID := Null_Snapshot;
Status : Rollback_Status := Not_Started;
Message : Unbounded_String;
Target_Snapshot : Snapshot_ID := Null_Snapshot;
-- Renamed from Snapshot_ID: a record component named identically to
-- the type Snapshot_ID is a SPARK/Ada name clash and was one of the
-- legality errors that blocked GNATprove Phase 2.
end record;

-- ═══════════════════════════════════════════════════════════════════════
-- SPARK Contracts for Reversibility Guarantees
-- Reversibility Obligations (UNPROVEN — tracked proof debt)
-- ═══════════════════════════════════════════════════════════════════════

-- Ghost functions for formal verification
function Snapshot_Exists (ID : Snapshot_ID) return Boolean
with Ghost;
-- True if snapshot with given ID exists in the system

function Snapshot_Is_Valid (ID : Snapshot_ID) return Boolean
with Ghost,
Post => (if Snapshot_Is_Valid'Result then Snapshot_Exists (ID));
-- True if snapshot exists and is in Valid state

function System_State_Matches_Snapshot (ID : Snapshot_ID) return Boolean
with Ghost,
Pre => Snapshot_Exists (ID);
-- True if current system state matches the snapshot

function Current_System_State_ID return Snapshot_ID
with Ghost;
-- Conceptual ID of current system state (for verification)
--
-- These properties are NOT formally verified. They were previously
-- expressed as SPARK ghost functions with stub bodies (return True),
-- which constitutes proof theatre. They are recorded here as prose
-- obligations and tracked in PROOF-NEEDS.md for an Idris2 model:
--
-- O-REV-1 Snapshot_Exists: a snapshot referenced by a non-null
-- Snapshot_ID is actually present in backend storage.
-- O-REV-2 Snapshot_Is_Valid: a "Valid"-state snapshot is restorable.
-- O-REV-3 System_State_Matches_Snapshot: after a Completed rollback,
-- on-disk system state equals the snapshot's captured state
-- (rollback atomicity / no partial state).
-- O-REV-4 Monotonic snapshot IDs: issued IDs strictly increase and
-- are never reused after deletion.
--
-- None of O-REV-1..4 are checked at compile time or runtime today.

-- ═══════════════════════════════════════════════════════════════════════
-- Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,21 @@ package body Snapshot_Manager is
Result := (
Status => Completed,
Message => To_Unbounded_String ("Native rollback complete"),
Snapshot_ID => ID
Target_Snapshot => ID
);

when Platform_Detection.Btrfs | Platform_Detection.ZFS =>
Result := (
Status => Requires_Reboot,
Message => To_Unbounded_String ("Snapshot restored, reboot required"),
Snapshot_ID => ID
Target_Snapshot => ID
);

when others =>
Result := (
Status => Failed,
Message => To_Unbounded_String ("Manual rollback required"),
Snapshot_ID => ID
Target_Snapshot => ID
);
end case;

Expand All @@ -209,7 +209,7 @@ package body Snapshot_Manager is
Result := (
Status => Failed,
Message => To_Unbounded_String ("Snapshot not found"),
Snapshot_ID => Null_Snapshot
Target_Snapshot => Null_Snapshot
);
end Rollback_To_Snapshot;

Expand All @@ -221,7 +221,7 @@ package body Snapshot_Manager is
Result := (
Status => Not_Started,
Message => Ada.Strings.Unbounded.To_Unbounded_String ("No snapshots"),
Snapshot_ID => Null_Snapshot
Target_Snapshot => Null_Snapshot
);
else
Rollback_To_Snapshot (Snapshots (Snapshot_Count).ID, Result);
Expand Down
Loading
Loading