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
127 changes: 109 additions & 18 deletions .github/workflows/audit-and-sign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ jobs:
python tools/sign_policy.py

- name: Generate signed certificate + HTML
id: generate_certificate
if: always()
env:
SDL_PRIVATE_KEY_PEM: ${{ secrets.SDL_PRIVATE_KEY_PEM }}
Expand All @@ -309,28 +310,75 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
python tools/generate_certificate.py

output_file="$(mktemp)"
python tools/generate_certificate.py | tee "$output_file"

cert_path="$(
sed -n 's/^OUTPUT_AUDIT_JSON=//p' "$output_file" |
tail -n 1
)"

case "$cert_path" in
proofs/latest-audit.json|proofs/local-audit.json)
;;
*)
echo "ERROR: generate_certificate.py did not emit a valid OUTPUT_AUDIT_JSON path."
exit 1
;;
esac

if [ ! -f "$cert_path" ]; then
echo "ERROR: generated certificate does not exist: $cert_path"
exit 1
fi

echo "audit_json=$cert_path" >> "$GITHUB_OUTPUT"
echo "Current-run certificate: $cert_path"

- name: Validate certificate contract
if: always()
env:
AUDIT_JSON: ${{ steps.generate_certificate.outputs.audit_json }}
run: |
set -euo pipefail
python tools/validate_certificate_contract.py proofs/latest-audit.json

if [ -z "${AUDIT_JSON:-}" ] || [ ! -f "$AUDIT_JSON" ]; then
echo "SKIP: generate_certificate.py did not produce a current-run certificate path."
exit 0
fi

python tools/validate_certificate_contract.py "$AUDIT_JSON"

- name: Verify certificate signature
if: always()
env:
AUDIT_JSON: ${{ steps.generate_certificate.outputs.audit_json }}
run: |
set -euo pipefail
python tools/verify_certificate.py proofs/latest-audit.json

if [ -z "${AUDIT_JSON:-}" ] || [ ! -f "$AUDIT_JSON" ]; then
echo "SKIP: generate_certificate.py did not produce a current-run certificate path."
exit 0
fi

python tools/verify_certificate.py "$AUDIT_JSON"

- name: Archive this run (P6 truth-preserving per-run archive)
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true'
env:
SDL_PRIVATE_KEY_PEM: ${{ secrets.SDL_PRIVATE_KEY_PEM }}
AUDIT_JSON: ${{ steps.generate_certificate.outputs.audit_json }}
run: |
set -euo pipefail

if [ -z "${AUDIT_JSON:-}" ] || [ ! -f "$AUDIT_JSON" ]; then
echo "SKIP: generate_certificate.py did not produce a current-run certificate path."
exit 0
fi

python tools/publish_run.py \
--cert proofs/latest-audit.json \
--cert "$AUDIT_JSON" \
--copy proofs/itgl_ledger.jsonl \
--copy proofs/itgl_final_hash.txt \
--copy proofs/latest-attempts.log \
Expand Down Expand Up @@ -377,6 +425,7 @@ jobs:
GITHUB_SHA: ${{ github.sha }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
AUDIT_JSON: ${{ steps.generate_certificate.outputs.audit_json }}
run: |
set -euo pipefail

Expand All @@ -385,7 +434,7 @@ jobs:
harmless="$(cat harmless_blocked.txt 2>/dev/null || echo 0)"

inconclusive="false"
if [ ! -f "proofs/latest-audit.json" ]; then inconclusive="true"; fi
if [ "${AUDIT_JSON:-}" != "proofs/latest-audit.json" ]; then inconclusive="true"; fi
if [ ! -f "proofs/run_summary.json" ]; then inconclusive="true"; fi
if [ ! -f "proofs/itgl_final_hash.txt" ]; then inconclusive="true"; fi
if ! grep -q '^ITGL_FINAL_HASH=' itgl_env.txt 2>/dev/null; then inconclusive="true"; fi
Expand Down Expand Up @@ -428,7 +477,7 @@ jobs:
echo "LATEST_CI_RUN_URL=${ci_run_url}" >> "$GITHUB_ENV"

- name: Publish latest PASS proof to docs/ (PASS only)
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true' && env.AUDIT_PASS == 'true' && env.INCONCLUSIVE != 'true'
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true' && steps.generate_certificate.outputs.audit_json == 'proofs/latest-audit.json'
run: |
set -euo pipefail
test -f proofs/latest-audit.html
Expand All @@ -445,6 +494,8 @@ jobs:
path: |
proofs/latest-audit.json
proofs/latest-audit.html
proofs/local-audit.json
proofs/local-audit.html
proofs/itgl_ledger.jsonl
proofs/itgl_final_hash.txt
proofs/run_summary.json
Expand All @@ -461,6 +512,8 @@ jobs:

- name: Commit proof files back to repo (archive always; latest PASS + sidecars only)
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true'
env:
AUDIT_JSON: ${{ steps.generate_certificate.outputs.audit_json }}
run: |
set -euo pipefail

Expand All @@ -484,7 +537,7 @@ jobs:

git add proofs/runs/ docs/runs/ docs/latest-run.json docs/monitor-summary.json

if [ "${AUDIT_PASS}" = "true" ]; then
if [ "${AUDIT_JSON:-}" = "proofs/latest-audit.json" ]; then
git add proofs/latest-audit.json proofs/latest-audit.html || true
git add docs/latest-audit.json docs/latest-audit.html || true
git add policy/isc_policy.signed.json || true
Expand Down Expand Up @@ -635,6 +688,7 @@ jobs:
PY

- name: Run paired benchmark through SIR
id: run_benchmark
env:
DISPATCH_PACK: ${{ github.event.inputs.pack || 'generic_safety' }}
DISPATCH_PROVIDER: ${{ github.event.inputs.provider || 'xai' }}
Expand Down Expand Up @@ -670,7 +724,28 @@ jobs:
fi
fi

sir benchmark run --mode "$mode" --pack "$pack" --provider "$provider" --model "$model"
output_file="$(mktemp)"
sir benchmark run --mode "$mode" --pack "$pack" --provider "$provider" --model "$model" |
tee "$output_file"

gated_run_id="$(
sed -n 's/^GATED_RUN_ID=//p' "$output_file" |
tail -n 1
)"

if [ -z "$gated_run_id" ]; then
echo "ERROR: benchmark did not emit GATED_RUN_ID."
exit 1
fi

gated_cert="proofs/runs/$gated_run_id/audit.json"
if [ ! -f "$gated_cert" ]; then
echo "ERROR: gated benchmark certificate not found: $gated_cert"
exit 1
fi

echo "gated_run_id=$gated_run_id" >> "$GITHUB_OUTPUT"
echo "gated_audit_json=$gated_cert" >> "$GITHUB_OUTPUT"

- name: Prepare docs/ for GitHub Pages
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true'
Expand Down Expand Up @@ -766,31 +841,45 @@ jobs:
print(json.dumps(latest_run_payload, ensure_ascii=False, indent=2))
PY

- name: Compute benchmark gated PASS truth from latest audit proof
- name: Compute benchmark gated PASS truth from current gated audit proof
id: compute_benchmark_gated_pass
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true'
env:
GATED_AUDIT_JSON: ${{ steps.run_benchmark.outputs.gated_audit_json }}
run: |
set -euo pipefail
python - <<'PY'
import json
import os
from pathlib import Path

cert_path = Path("proofs/latest-audit.json")
gated_path_raw = os.environ.get("GATED_AUDIT_JSON", "").strip()
canonical_path = Path("proofs/latest-audit.json")
gated_pass = False

if cert_path.is_file():
payload = json.loads(cert_path.read_text(encoding="utf-8"))
result = str(payload.get("result", "")).strip().upper()
gated_pass = (result == "AUDIT PASSED")

if gated_path_raw:
gated_path = Path(gated_path_raw)
if gated_path.is_file():
gated_bytes = gated_path.read_bytes()
payload = json.loads(gated_bytes)
result = str(payload.get("result", "")).strip().upper()
gated_pass = (
result == "AUDIT PASSED"
and canonical_path.is_file()
and canonical_path.read_bytes() == gated_bytes
)

value = "true" if gated_pass else "false"
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env_file:
env_file.write(f"BENCHMARK_GATED_PASS={'true' if gated_pass else 'false'}\n")
env_file.write(f"BENCHMARK_GATED_PASS={value}\n")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output_file:
output_file.write(f"gated_pass={value}\n")

print(f"BENCHMARK_GATED_PASS={'true' if gated_pass else 'false'}")
print(f"BENCHMARK_GATED_PASS={value}")
PY

- name: Publish latest PASS proof to docs/ (benchmark gated PASS only)
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true' && env.BENCHMARK_GATED_PASS == 'true'
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true' && steps.compute_benchmark_gated_pass.outputs.gated_pass == 'true'
run: |
set -euo pipefail
test -f proofs/latest-audit.html
Expand All @@ -814,6 +903,8 @@ jobs:

- name: Commit benchmark files back to repo (archive + benchmark index/pairs)
if: always() && env.CANONICAL_PUBLICATION_CONTEXT == 'true'
env:
BENCHMARK_GATED_PASS: ${{ steps.compute_benchmark_gated_pass.outputs.gated_pass }}
run: |
set -euo pipefail
git config user.name "SIR Audit Bot"
Expand Down
35 changes: 30 additions & 5 deletions .github/workflows/r1-cli-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,50 @@ jobs:
PY

- name: Generate cert from scenario run and validate certificate contract
id: generate_scenario_certificate
env:
SDL_PRIVATE_KEY_PEM: ${{ env.SDL_PRIVATE_KEY_PEM }}
run: |
set -euo pipefail
python tools/generate_certificate.py
python tools/validate_certificate_contract.py proofs/latest-audit.json

output_file="$(mktemp)"
python tools/generate_certificate.py | tee "$output_file"

cert_path="$(
sed -n 's/^OUTPUT_AUDIT_JSON=//p' "$output_file" |
tail -n 1
)"

case "$cert_path" in
proofs/latest-audit.json|proofs/local-audit.json)
;;
*)
echo "ERROR: generate_certificate.py did not emit a valid OUTPUT_AUDIT_JSON path."
exit 1
;;
esac

test -f "$cert_path"
python tools/validate_certificate_contract.py "$cert_path"
echo "audit_json=$cert_path" >> "$GITHUB_OUTPUT"

- name: CLI verify smoke (cert path + stdin after scenario)
env:
AUDIT_JSON: ${{ steps.generate_scenario_certificate.outputs.audit_json }}
run: |
set -euo pipefail
sir verify cert --key /tmp/sir_ci_pub.pem --key-registry /tmp/ci_missing_key_registry.json proofs/latest-audit.json
cat proofs/latest-audit.json | sir verify cert --key /tmp/sir_ci_pub.pem --key-registry /tmp/ci_missing_key_registry.json -
test -f "$AUDIT_JSON"
sir verify cert --key /tmp/sir_ci_pub.pem --key-registry /tmp/ci_missing_key_registry.json "$AUDIT_JSON"
cat "$AUDIT_JSON" | sir verify cert --key /tmp/sir_ci_pub.pem --key-registry /tmp/ci_missing_key_registry.json -

- name: Create archive and verify archive via CLI
env:
SDL_PRIVATE_KEY_PEM: ${{ env.SDL_PRIVATE_KEY_PEM }}
AUDIT_JSON: ${{ steps.generate_scenario_certificate.outputs.audit_json }}
run: |
set -euo pipefail
python tools/publish_run.py --cert proofs/latest-audit.json \
test -f "$AUDIT_JSON"
python tools/publish_run.py --cert "$AUDIT_JSON" \
--copy proofs/itgl_ledger.jsonl \
--copy proofs/itgl_final_hash.txt \
--copy proofs/latest-attempts.log \
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SIR: Signal Integrity Resolver Version 2.1
# SIR: Signal Integrity Resolver Version 2.2

[![SIR Real Governance Audit](https://github.com/SDL-HQ/sir-firewall/actions/workflows/audit-and-sign.yml/badge.svg)](https://github.com/SDL-HQ/sir-firewall/actions/workflows/audit-and-sign.yml)

Expand Down Expand Up @@ -204,12 +204,13 @@ SIR’s job is simple: enforce policy before inference, then prove what happened
* [Minimal pilot runbook](docs/minimal-pilot-runbook.md) (procedural cold-start path)
* [Evaluator technical explainer](docs/evaluator-technical-explainer.md) (evaluation and interpretation path)
* [Assurance kit](docs/assurance-kit.md) (supporting evaluation and verification reference)
* [Evidence perimeter note](docs/evidence-perimeter.v2.md) (current bounded benchmark perimeter)
* [Evidence perimeter note](docs/evidence-perimeter.v4.md) (current bounded benchmark perimeter)
* [Public backlog](docs/backlog.md) (known constraints and planned hardening)
* [External technical review preparation](docs/external-technical-review-prep.md)
* [Engineer guide](docs/engineer-guide.md) (local runs, signing, serving)
* [Trial guide](docs/trial-guide.md) (auditors, insurers, evidence capture)
* [Key governance readiness](docs/key-governance-readiness.md) (authority map and `CRYPTO_ENFORCED` checklist)
* [Release notes](docs/release-notes-2.1.md) (2.1 closeout)
* [Release notes](docs/release-notes-2.2.md) (2.2 closeout)
* [Retention / Tier B export](RETENTION.md)

---
Expand Down
72 changes: 72 additions & 0 deletions docs/backlog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SIR Public Backlog

This backlog tracks known hardening items, constraints, and planned work. Inclusion does not imply a committed release date.

SIR separates current implemented behaviour from planned hardening work and future candidate coverage.

## Status labels

- Current
- Planned
- Investigating
- Known constraint
- Future candidate
- Not in scope

## Current domain packs

| Pack | Status | Notes |
|---|---|---|
| generic_safety | Current | Baseline coverage for bypass attempts, harmful requests, and prompt injection patterns. |
| data_exfiltration_pressure | Current | Attempts to extract restricted content, secrets, or internal data through pressure and reframing. |
| support_operator_override | Current | Override-style prompts invoking authority, support escalation, or operator language. |
| eu_ai_act_compliance_pressure | Current | Pressure patterns testing whether stated governance, disclosure, and control boundaries hold. Bounded partial-coverage pack, not a full-pass pack. |
| hipaa_mental_health | Current | Domain pack for HIPAA-bound mental health workloads. |
| pci_payments | Current | Domain pack for PCI-style payment workloads. |

## Evidence hardening backlog

| Item | Status | Notes |
|---|---|---|
| Pack hash self-computation | Planned | load_domain_pack() does not compute a content hash of the loaded pack file. pack_hash is populated only when supplied by a caller via pack_identity_context. |
| Raw pre-normalisation obfuscation signal | Planned | obfuscation_signal_detected is evaluated on post-normalisation text. Payloads containing only invisible Unicode with no signal keywords normalise silently without the signal firing. |
| Granular early-exit ITGL components | Planned | Mixed-mode, malformed ISC, and structured schema declaration failures do produce ITGL entries, but reuse generic component types with the specific error code carried in entry data. Dedicated component types per error class are not implemented. |
| Version-source consistency check | Planned | No automated check prevents package and artefact version drift. |
| Provider response trace metadata | Investigating | Non-sensitive provider response id, model, and token metadata for live evidence. |
| Transient provider retry policy | Investigating | Limited retries for provider or server errors without concealing final failures. |
| RSA-PSS signature padding | Planned | Certificate signing and verification currently use PKCS1v15. RSA-PSS is the recommended padding for new systems. |
| CJK token estimation | Planned | _estimate_tokens() uses a character-based approximation that underestimates token count for CJK text, making friction limits more permissive for non-whitespace-delimited languages. |

## Known constraints

| Item | Status | Notes |
|---|---|---|
| Structured schema extensibility | Known constraint | Adding new structured schema types requires multiple explicit change points. Intentional for v1. |
| GitHub dependent dropdowns | Known constraint | GitHub Actions does not support dynamic dependent dropdowns. Invalid provider and model combinations fail closed in validation. |
| Deterministic rule scope | Known constraint | SIR evaluates deterministic rules over declared ingress content. It does not infer hidden downstream agent state. |
| strict_isc flag | Known constraint | STRICT_ISC_ENFORCEMENT is retained for policy and pack schema compatibility. ISC structure rejection is unconditional and no longer gated by this flag. |

## Future pack candidates

Possible expansion areas only. Not current implemented coverage.

| Candidate | Status |
|---|---|
| Tool result injection pressure | Future candidate |
| Mental health clinical | Future candidate |
| PII protection | Future candidate |
| Financial services | Future candidate |
| Legal and contracts | Future candidate |
| Insurance and underwriting | Future candidate |
| Code generation safety | Future candidate |

## Not in scope

| Item | Notes |
|---|---|
| Semantic prompt-injection detection | SIR remains deterministic and rules-based. |
| Universal agentic-injection prevention | SIR evaluates declared ingress content. It does not control every downstream agent path. |
| Model safety certification | SIR produces gate and evidence artefacts, not model safety certification. |
| Content SIR never receives | SIR cannot evaluate content outside its declared ingress path. |
| API message-ordering validation | SIR does not validate API-level message ordering or role sequence. Deployments using self-hosted inference should independently enforce message-ordering validation. |
| Multi-turn context accumulation | SIR evaluates declared ingress at the request boundary. It does not evaluate state accumulated across conversation turns. |
Loading