From 6050ce65a6d7f8145d43dbbd15e3e4348de4f249 Mon Sep 17 00:00:00 2001 From: Aadithya Vishnu Date: Wed, 9 Sep 2026 01:53:22 +0100 Subject: [PATCH 1/3] compliance: EU AI Act conformance pass (Art. 15(5), Art. 27 FRIA, Digital Omnibus) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full re-audit of the compliance layer against Regulation (EU) 2024/1689 as amended by the Digital Omnibus (Regulation (EU) 2026/1744), plus multi-source cross-check (EUR-Lex, artificialintelligenceact.eu, CEN-CENELEC JTC 21). Citation fixes - Cybersecurity is Art. 15(5), not 15(4) (15(4) = robustness / feedback loops in the enacted text). Corrected across credential_vault, manifest, backbone and ~10 doc files. BatchNode state-isolation stays at 15(4) β€” that one really is robustness. Least privilege reframed as a proportionate supporting measure. - Docs still said "Art. 9 FRIA" / "24h-72h" / "Recital 12" β€” synced to the Aug-2025 code fixes and to the enacted text. New capability - Article27FRIANode: the actual Art. 27 deployer Fundamental Rights Impact Assessment. Scope-gated on deployer_class (PUBLIC_BODY / PRIVATE_PUBLIC_SERVICE / CREDIT_SCORING / LIFE_HEALTH_INSURANCE); generates and completeness-checks the 27(1)(a)-(f) template, records the Art. 27(3) authority-notification duty and an Art. 27(4) DPIA cross-reference; as_markdown() -> fria_art27.md. FundamentalRightsImpactNode stays as the Art. 9(2)(a) runtime screen; the two are now clearly separated everywhere. Human oversight - no more structural rubber-stamp - HumanJuryNode.human_decision_provider (additive): a real out-of-band decision (web form / Slack / API / resume_human_decision) that keeps the validated + AuthorityLedger path without needing a TTY. - Backbone jury default flipped auto_first_choice -> always_human. With no provider and no TTY the jury HALTS by design. The showcase's _mock_inputs now builds a simulated reviewer provider, with a loud DEMO banner. Digital Omnibus - ProhibitedPracticeGuard: added NCII and CSAM categories (Art. 5, applicable 2 Dec 2026; include_omnibus_categories=False to disable). - Timeline updated repo-wide: standalone Annex III high-risk obligations apply from 2 Dec 2027; Annex I embedded from 2 Aug 2028; Art. 5 / GPAI / Art. 50 unaffected. README badge "Ready Aug 2026" -> "evidence infrastructure"; dropped the "First EU AI Act-Ready" tagline. Precision / downscoping - SyntheticMarkerNode: METADATA = Art. 50(2) machine-readable; VISIBLE = Art. 50(1)/(4) human disclosure (was mis-lumped as 50(2)). Version string no longer hardcoded. - IncidentReporterNode: HIGH no longer pinned to the 240h "death" deadline; conservative 48h/360h ceiling + all three Art. 73 legal deadlines + provider_must_confirm_applicable_paragraph on every record. - BiasFilterNode / manifest / IncidentReporter / SupplierAgreementRegistry: claims scoped to what the code does (keyword gate, Annex IV Sec.2(b), PMM evidence not the PMM plan, FOSS carve-out - Lar itself is outside Art. 25(4)). Showcase + tests - 22_eu_ai_act_finance_showcase.py rewritten: 26 rows, Omnibus panel, DEMO-mode note, LAR_SHOWCASE_MODEL env override. Verified 24/24 runtime checks pass. - New tests/unit/test_compliance_conformance_fixes.py (23 tests) locking in every fix. Full suite green. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --- README.md | 55 +- docs/compliance.md | 82 ++- docs/compliance/annex-iv-template.md | 4 +- docs/compliance/auditor_guide.md | 30 +- docs/compliance/enterprise-reference.md | 33 +- docs/compliance/eu-ai-act-deep-dive.md | 29 +- docs/compliance/finance-showcase.md | 39 +- docs/compliance/paper-compliance-mapping.md | 78 ++- docs/core-concepts/4-core-patterns.md | 2 +- docs/guides/build-compliant-agent.md | 6 +- docs/index.md | 2 +- .../22_eu_ai_act_finance_showcase.py | 502 ++++++++---------- src/lar/compliance/__init__.py | 2 + src/lar/compliance/bias_filter.py | 13 +- src/lar/compliance/credential_vault.py | 24 +- src/lar/compliance/fria_art27_node.py | 280 ++++++++++ src/lar/compliance/incident_reporter.py | 64 ++- src/lar/compliance/manifest.py | 26 +- .../compliance/prohibited_practice_guard.py | 92 +++- .../compliance/supplier_agreement_registry.py | 17 +- src/lar/compliance/synthetic_marker.py | 86 ++- src/lar/enterprise/backbone.py | 227 ++++++-- src/lar/enterprise/run.py | 40 +- src/lar/node.py | 48 +- .../unit/test_compliance_conformance_fixes.py | 301 +++++++++++ 25 files changed, 1568 insertions(+), 514 deletions(-) create mode 100644 src/lar/compliance/fria_art27_node.py create mode 100644 tests/unit/test_compliance_conformance_fixes.py diff --git a/README.md b/README.md index 2cbcfed..ec8aeca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

LΓ‘r Logo

-

LΓ‘r: The PyTorch for Agents β€” The First EU AI Act-Ready Agent Execution Engine

+

LΓ‘r: The PyTorch for Agents β€” an agent execution engine built for EU AI Act evidence

PyPI - Version @@ -10,16 +10,18 @@ PyPI - Downloads - EU AI Act Ready + EU AI Act evidence infrastructure Sponsor

-# LΓ‘r β€” An EU AI Act-Ready Agent Execution Engine +# LΓ‘r β€” an agent execution engine built for EU AI Act evidence -The EU AI Act's Art. 50/GPAI/penalty regime took effect August 2, 2026. Teams building AI agents in finance, healthcare, legal, and enterprise need to prove to regulators exactly what their agent did, why, and what it cost β€” on every run. LΓ‘r is built for that from the ground up. ("First" / "existing frameworks cannot do this" claims about the entire framework landscape were removed here β€” not independently verifiable, and other frameworks can be wired to produce audit trails too, typically via a separate add-on rather than the executor itself.) +Teams building AI agents in finance, healthcare, legal, and enterprise need to prove to regulators exactly what their agent did, why, and what it cost β€” on every run. LΓ‘r is built for that from the ground up. ("First" / "existing frameworks cannot do this" claims were removed β€” other frameworks can be wired to produce audit trails too, typically via a separate add-on rather than the executor itself.) + +**Where the AI Act stands (post Digital Omnibus, Regulation (EU) 2026/1744, in force 27 Jul 2026):** Article 5 prohibitions apply since Feb 2025 (non-consensual intimate imagery and AI-CSAM added from 2 Dec 2026); GPAI obligations since Aug 2025; **Article 50 transparency since 2 Aug 2026** (2 Dec 2026 grace for the machine-readable 50(2) marking); **standalone Annex III high-risk obligations apply from 2 December 2027** (deferred from 2 Aug 2026), and Annex I embedded high-risk from 2 Aug 2028. LΓ‘r's role in all of this is to produce the *evidence* β€” it is a software component in the Art. 25 value chain, not the "provider" (see [Compliance & Safety](#compliance--safety)). LΓ‘r is a ground-up agent execution engine where **auditability is structural, not an add-on**. Every design decision β€” deterministic graphs, step-level state diffs, cryptographic audit trails, topology validation, human oversight primitives β€” exists to satisfy EU AI Act requirements out of the box. @@ -34,7 +36,7 @@ LΓ‘r is the execution spine of a three-part system. Each repo is independent and | Repository | Role | | :--- | :--- | -| **[LΓ‘r](https://github.com/snath-ai/lar)** ← you are here | The execution spine β€” deterministic graph engine, 20 EU AI Act compliance primitives, HMAC audit trail | +| **[LΓ‘r](https://github.com/snath-ai/lar)** ← you are here | The execution spine β€” deterministic graph engine, ~24 EU AI Act compliance primitives, HMAC audit trail | | **[LΓ‘r-JEPA](https://github.com/snath-ai/Lar-JEPA)** | The world model β€” 10 ABCs spanning the full inference-time contract (divergence routing, modal encoding, fault localisation, adapter routing) | | **[LΓ‘r DMN](https://github.com/snath-ai/DMN)** | The memory blueprint β€” `AbstractDMN` + `AbstractAdapterRouter`; domain implementations own storage and LoRA fitting; zero runtime dependencies | @@ -42,9 +44,9 @@ The industry is building the Brain (LLMs, JEPAs). We are building the Nervous Sy --- -## The 21 EU AI Act Compliance Primitives (v2.2.0) +## The EU AI Act Compliance Primitives -LΓ‘r ships a complete **Enterprise Compliance Backbone** β€” 21 primitives covering all requirements mapped by Nannini et al. (2026), the definitive compliance architecture paper for AI agents under EU law. Validated live against `ollama/phi4:latest` and `ollama/llama3.2:latest`: +LΓ‘r ships an **Enterprise Compliance Backbone** β€” a set of primitives, each mapped to a provision of Regulation (EU) 2024/1689 (as amended by the Digital Omnibus, Regulation (EU) 2026/1744). "Mapped" means a runtime hook fires and/or an evidence artifact is produced β€” it is **not** a conformity assessment, and it does not by itself discharge the obligation. Architecture cross-referenced to Nannini et al. (2026). Validated live against `ollama/phi4:latest` (override with `LAR_SHOWCASE_MODEL`): **Core primitives (v2.1.x)** @@ -53,29 +55,30 @@ LΓ‘r ships a complete **Enterprise Compliance Backbone** β€” 21 primitives cover | **Art. 12** | Causal audit logging + per-step integrity | `GraphExecutor` β†’ HMAC-SHA256 signed JSON trace + `verify_step_integrity()` | | **Art. 14** | Human oversight interrupt + automation boundary | `HumanJuryNode(automation_boundary=…)` + `AuthorityLedger` (Fourth Tier) | | **Art. 3(23)** | Substantial modification guard | `AdaptiveNode` + `TopologyValidator` + `RuntimeStateVersioner` | -| **Art. 9** | Risk management gate + FRIA | `RiskScorerNode` + `PolicyRegistry` + `FundamentalRightsImpactNode` | -| **Art. 15(4)** | JIT + trust-based privilege minimisation | `CredentialVault` β€” `get()` and `get_with_trust()` | +| **Art. 9(2)(a)** | Risk-management gate + runtime fundamental-rights screen | `RiskScorerNode` + `PolicyRegistry` + `FundamentalRightsImpactNode` *(not the Art. 27 FRIA β€” see below)* | +| **Art. 15(5)** | Cybersecurity β€” NHI least privilege (JIT + trust-gated) | `CredentialVault` β€” `get()` and `get_with_trust()` | | **GDPR Art. 17** | PII redaction + erasable per-subject memory | `PIIRedactionEngine` + `SessionMemoryNode` | -| **Art. 50(2)** | Synthetic content marking | `SyntheticMarkerNode` | -| **prEN 18283** | Runtime bias detection | `BiasFilterNode` | +| **Art. 50(2) / 50(1)/(4)** | Synthetic content marking (machine-readable + visible) | `SyntheticMarkerNode` (`METADATA` / `VISIBLE`) | +| **Art. 10(2)(f)-(g)** | Runtime bias keyword gate (supports prEN 18283) | `BiasFilterNode` | | **Art. 14 (fractal)** | Meaningful HITL in parallel agents | `BranchTriageNode` | | **AEPD Rule of 2** | Lethal trifecta block | `LethalTrifectaGuard` | | **Art. 50 / Art. 13** | Third-party + deployer transparency | `TransparencyEngine` + `DeployerTransparencyNode` | | **Step 9** | External action inventory + adjacent legislation | `ComplianceManifestGenerator` (auto-detects DORA, MiFID II, ePrivacy…) | | **Art. 12/14** | Stakeholder authority record | `AuthorityLedger` | -**New in v2.2.0 β€” gap closure** +**Gap-closure primitives** | Article | Requirement | LΓ‘r Primitive | | :--- | :--- | :--- | -| **Art. 9 FRIA** | Fundamental rights impact (6 EU Charter dimensions) | `FundamentalRightsImpactNode` | -| **Art. 9 PMM** | Stochastic output variance monitoring | `BehavioralEnvelopeMonitor` | -| **Art. 25(4)** | Written supplier agreement enforcement | `SupplierAgreementRegistry` | +| **Art. 27** | **Deployer** Fundamental Rights Impact Assessment (the *named* FRIA) | `Article27FRIANode` β€” scope-gated on `deployer_class`; generates & completeness-checks the 27(1)(a)-(f) template; writes `fria_art27.md` | +| **Art. 9(2)(a)** | Runtime fundamental-rights screen (6 EU Charter dimensions) β€” *not* Art. 27 | `FundamentalRightsImpactNode` | +| **Art. 9(9) PMM** | Stochastic output variance monitoring | `BehavioralEnvelopeMonitor` | +| **Art. 25(4)** | Written supplier agreement enforcement (FOSS carve-out β€” LΓ‘r itself is exempt) | `SupplierAgreementRegistry` | | **Art. 3(23)** | Post-conformity tool addition detection | `DynamicToolDiscoveryMonitor` | | **Art. 3** | Sub-agent boundary classification | `MultiAgentBoundaryNode` | -| **Art. 73–74** | Real-time incident detection + reporting deadlines | `IncidentReporterNode` (executor hook β€” fires automatically) | -| **Art. 5** | Prohibited practice guard | `ProhibitedPracticeGuard` (executor hook β€” fires automatically) | -| **Art. 15(5)** | Cybersecurity & prompt injection guard | `PromptInjectionGuard` (adversarial input interception) | +| **Art. 73** | Real-time incident detection (conservative 48h/360h ceiling + all three Art. 73 legal deadlines per record) | `IncidentReporterNode` (executor hook) | +| **Art. 5** (incl. 2 Dec 2026 Omnibus) | Prohibited-practice guard β€” social scoring, manipulation, vulnerability exploitation, **NCII**, **CSAM** | `ProhibitedPracticeGuard` (executor hook) | +| **Art. 15(5)** | Cybersecurity & prompt-injection guard | `PromptInjectionGuard` (adversarial input interception) | **[Read the EU AI Act Deep Dive β†’](https://docs.snath.ai/compliance/eu-ai-act-deep-dive/)** | **[Nannini et al. (2026) Full Mapping β†’](https://docs.snath.ai/compliance/paper-compliance-mapping/)** | **[Real, Executed Failure-Mode Tests β†’](examples/failure_modes/)** @@ -83,7 +86,7 @@ LΓ‘r ships a complete **Enterprise Compliance Backbone** β€” 21 primitives cover ## See It Running: The Finance Showcase -One command runs a live SME credit decision through the compliance primitives and produces three HMAC-signed audit artefacts: +One command runs a live SME credit decision through the compliance primitives and produces HMAC-signed audit artefacts (causal trace, authority ledger, action-inventory manifest) plus the deployer Art. 27 FRIA: ```bash python examples/compliance/22_eu_ai_act_finance_showcase.py @@ -105,7 +108,7 @@ Every step produces a real Article 12 causal trace, a real Article 14 AuthorityL **[Full showcase breakdown β€” execution trace, real JSON artefacts, 12-step coverage map β†’](https://docs.snath.ai/compliance/finance-showcase/)** -> **Open-source vs. enterprise:** All 20 compliance primitives (`FundamentalRightsImpactNode`, `IncidentReporterNode`, `BehavioralEnvelopeMonitor`, `SessionMemoryNode`, `BranchTriageNode`, `CredentialVault`, etc.) are in `lar.compliance` and fully open-source under Apache 2.0. The finance showcase uses `build_and_run` from `lar.enterprise`, a convenience wrapper in the enterprise tier. Open-source users assemble the same pipeline from primitives β€” the step-by-step guide walks you through it: **[Build a Compliant Agent from Scratch β†’](https://docs.snath.ai/guides/build-compliant-agent/)** +> **Open-source vs. enterprise:** All ~24 compliance primitives (`FundamentalRightsImpactNode`, `IncidentReporterNode`, `BehavioralEnvelopeMonitor`, `SessionMemoryNode`, `BranchTriageNode`, `CredentialVault`, etc.) are in `lar.compliance` and fully open-source under Apache 2.0. The finance showcase uses `build_and_run` from `lar.enterprise`, a convenience wrapper in the enterprise tier. Open-source users assemble the same pipeline from primitives β€” the step-by-step guide walks you through it: **[Build a Compliant Agent from Scratch β†’](https://docs.snath.ai/guides/build-compliant-agent/)** ### The High-Stakes Loan Underwriter (Compliance-by-Design vs. LLM-as-Judge) @@ -147,7 +150,7 @@ This table was revised after an independent verification pass (this repo's own C | **Debugging** | Stack trace from inside `AgentExecutor`. | Exact node, exact error, exact state β€” in the log. | | **Auditability** | Built-in tracing exists; LangSmith is the polished, marketed observability product and has a paid tier. | Built-in. The `GraphExecutor` flight log is the audit trail β€” not an add-on, the control-flow mechanism itself. | | **Multi-Agent** | AutoGen's `GroupChat` / CrewAI's hierarchical process pick among pre-defined agents dynamically. | Deterministic assembly line β€” you define the exact path (or `AdaptiveNode` composes and *validates* new structure before running it β€” see below). | -| **Compliance** | No built-in EU AI Act primitives. | 20 compliance primitives, cryptographic logs, Art. 14 oversight β€” mapped to Nannini et al. (2026)'s 12-step architecture. | +| **Compliance** | No built-in EU AI Act primitives. | ~24 compliance primitives, cryptographic logs, Art. 14 oversight β€” mapped to Nannini et al. (2026)'s 12-step architecture. | | **Cost** | Costs an LLM call if you choose to route via a prompt (a common but not universal pattern). | Code-based routing β€” $0.00/route, by default. | | **Scale / recursion** | **Corrected**: LangGraph does NOT crash by default β€” verified directly, 25/26/100/500-step graphs all complete with no explicit config. `recursion_limit` is a real, opt-in safety feature, not a default trap. | Verified directly too: v2.2.3 fixed a real bug where LΓ‘r's own fatigue detection false-tripped on 60+ instances of the same reusable node class (see `examples/failure_modes/4_recursion_limit.py`). | | **Crash Recovery** | **Corrected**: LangGraph ships a real, automatic checkpointer β€” state is persisted every step, keyed by `thread_id`, and resumes from the last successful step automatically. This is currently *more* automatic than LΓ‘r's own resumability pattern (see next row). | LΓ‘r's state is a plain serializable dict, so save/restore is simple to build β€” but the current examples require the developer to manually track and hardcode the resume entry point (see `examples/patterns/9_resumable_graph.py`). Not automatic yet. | @@ -250,7 +253,7 @@ LΓ‘r v2.1.0 introduces native support for **Scientific and Biological AI Archite 1. **Route Tensors Deterministically:** Pass gigabyte-sized biological or scientific tensors across nodes without serialization crashes. 2. **Audit Latent Space:** Cryptographically log the exact multidimensional shape and type of massive data structures moving through the pipeline without blowing out memory (e.g., `{"__type__": "Tensor", "shape": [1, 768]}`). -LΓ‘r is the first EU AI Act-ready engine built not just for LLMs, but for real mathematical world models. For a complete heterogeneous cognitive architecture built on this foundation, see **[LΓ‘r-JEPA](https://github.com/snath-ai/Lar-JEPA)**. +LΓ‘r is built not just for LLMs, but for real mathematical world models. For a complete heterogeneous cognitive architecture built on this foundation, see **[LΓ‘r-JEPA](https://github.com/snath-ai/Lar-JEPA)**. --- @@ -354,7 +357,7 @@ for step in executor.run_step_by_step(adaptive, {"doc_type": "financial", "docum **The real unlock** is combining `AdaptiveNode` with `BatchNode`. The LLM can decide both how many workers to spawn *and* that they should run in parallel β€” covering case complexity you'd need a combinatorial explosion of static branches to replicate. -**This stays compliant regardless of graph shape.** Every generated spec is logged to the HMAC-signed Causal Trace before any node executes (Art. 12). `TopologyValidator` is pure Python β€” deterministic, not probabilistic β€” so the safety decision is never delegated to an LLM. All 13 compliance primitives (`HumanJuryNode`, `PIIRedactionEngine`, `BiasFilterNode`, etc.) work inside generated subgraphs exactly as they do in static ones. The graph shape changes at runtime; the compliance guarantees don't. +**This stays compliant regardless of graph shape.** Every generated spec is logged to the HMAC-signed Causal Trace before any node executes (Art. 12). `TopologyValidator` is pure Python β€” deterministic, not probabilistic β€” so the safety decision is never delegated to an LLM. All compliance primitives (`HumanJuryNode`, `PIIRedactionEngine`, `BiasFilterNode`, etc.) work inside generated subgraphs exactly as they do in static ones. The graph shape changes at runtime; the compliance guarantees don't. In fractal agents (manager `AdaptiveNode` + `BatchNode` of specialist `AdaptiveNode`s), wire `BranchTriageNode` between `BatchNode` and the human jury. Without it the human sees a rolled-up score β€” they don't know which parallel branch triggered the alert. That's not meaningful oversight under Art. 14. With it, the jury context includes per-branch findings before `ReduceNode` compresses them away. @@ -365,7 +368,7 @@ See: [`examples/adaptive/`](examples/adaptive/) | [`examples/compliance/23_fract ## Compliance & Safety > [!IMPORTANT] -> **Who is the "Provider"?** Under the EU AI Act (Art. 3), LΓ‘r is a software component, not an AI system. The organisation deploying a high-risk agent is the legal **Provider**. LΓ‘r provides the 13 architectural primitives to generate the *evidence* (audit logs, manifests, oversight records) required for a conformity assessment. +> **Who is the "Provider"?** Under the EU AI Act (Art. 3), LΓ‘r is a software component, not an AI system. The organisation deploying a high-risk agent is the legal **Provider**. LΓ‘r provides the compliance primitives to generate the *evidence* (audit logs, manifests, oversight records) required for a conformity assessment. > [!WARNING] > **Legal Disclaimer:** LΓ‘r is open-source infrastructure, not legal advice. Using LΓ‘r does not automatically guarantee compliance. Organisations are solely responsible for legal review and conformity assessments. @@ -404,7 +407,7 @@ python examples/compliance/11_verify_audit_log.py secure_logs/run_xyz.json your_ When you run a fully compliant, resumable agent (like `27_ultimate_resumable_enterprise.py`), the output isn't just print statementsβ€”it produces concrete, cryptographically sealed JSON artifacts designed for legal and compliance review. We have provided real output samples generated by `phi4:latest` in the [**`examples/compliance/sample_auditor_reports/`**](examples/compliance/sample_auditor_reports/) directory: -1. **`sample_compliance_manifest.json`**: A high-level regulatory map proving that all 23 EU AI Act primitives (bias filters, PII redactors, supplier registries) successfully fired during the run. +1. **`sample_compliance_manifest.json`**: A high-level regulatory map showing which EU AI Act primitives (bias filters, PII redactors, supplier registries) successfully fired during the run. 2. **`sample_authority_ledger.json`**: The immutable, cryptographically signed ledger proving *who* approved the high-risk action, *what* their role was, and *when* they approved it. 3. **`sample_causal_trace.json`**: A massive, detailed execution log capturing the exact state changes, inputs, and outputs of every single node in the graph (with PII automatically stripped) for deep forensic analysis. @@ -471,7 +474,7 @@ Compliance pattern library: | **13** | **[`12_transparency_disclosure.py`](examples/compliance/12_transparency_disclosure.py)** | Transparency Engine (Art. 13) | | **14** | **[`13_risk_scored_routing.py`](examples/compliance/13_risk_scored_routing.py)** | Risk-Scored Routing (Art. 14) | | **15** | **[`14_runtime_drift_detection.py`](examples/compliance/14_runtime_drift_detection.py)** | Drift Detection (Art. 3(23)) | -| **16** | **[`15_jit_credential_vault.py`](examples/compliance/15_jit_credential_vault.py)** | JIT Credential Vault (Art. 15(4)) | +| **16** | **[`15_jit_credential_vault.py`](examples/compliance/15_jit_credential_vault.py)** | JIT Credential Vault (Art. 15(5)) | | **17** | **[`16_pii_redaction.py`](examples/compliance/16_pii_redaction.py)** | PII Redaction (GDPR Art. 17) | | **18** | **[`17_causal_trace_logging.py`](examples/compliance/17_causal_trace_logging.py)** | Causal Trace Logging (Art. 12) | | **19** | **[`18_synthetic_content_marking.py`](examples/compliance/18_synthetic_content_marking.py)** | Synthetic Content Marking (Art. 50) | diff --git a/docs/compliance.md b/docs/compliance.md index f22dce5..6c05d4b 100644 --- a/docs/compliance.md +++ b/docs/compliance.md @@ -1,9 +1,9 @@ -# Compliance & Safety (EU AI Act Ready - Aug 2026) +# Compliance & Safety (EU AI Act β€” evidence infrastructure) !!! warning "Legal Disclaimer" LΓ‘r is open-source software infrastructure, not legal or compliance advice. Using LΓ‘r does not automatically guarantee compliance with the EU AI Act, GDPR, HIPAA, or any other regulation. Organizations are solely responsible for ensuring their AI systems undergo proper legal review and conformity assessments. -LΓ‘r is engineered to meet the stringent requirements of the **EU AI Act (2026)** and **FDA 21 CFR Part 11** for High-Risk AI Systems. +LΓ‘r is engineered to meet the requirements of the **EU AI Act (Regulation (EU) 2024/1689, as amended by the Digital Omnibus 2026/1744)** and **FDA 21 CFR Part 11** for High-Risk AI Systems. Unlike "Black Box" frameworks that obfuscate decision paths, LΓ‘r is a "Glass Box" engine designed for forensic auditability. @@ -11,7 +11,7 @@ Unlike "Black Box" frameworks that obfuscate decision paths, LΓ‘r is a "Glass Bo ## EU AI Act Alignment -The EU AI Act (fully enforceable August 2026) imposes strict obligations on "High-Risk" systems (e.g., Medical Devices, Employment, Credit Scoring, Critical Infrastructure). +The EU AI Act imposes obligations (standalone Annex III high-risk obligations apply from 2 December 2027 after the Digital Omnibus deferral; Art. 5 since Feb 2025, GPAI since Aug 2025, Art. 50 since Aug 2026) on "High-Risk" systems (e.g., Medical Devices, Employment, Credit Scoring, Critical Infrastructure). ### Understanding Your Role (Art. 3) @@ -36,28 +36,29 @@ LΓ‘r handles the **mechanical infrastructure** of compliance. It provides the ar In short: LΓ‘r provides the "flight recorder" and "emergency brakes." The organization must bring the safe model, the responsible human operators, and the governance policies. * **LΓ‘r (The Framework):** LΓ‘r acts as a component supplier. We provide the architectural primitives (nodes, executor, loggers) that generate the forensic *evidence* you need to pass a conformity assessment. -LΓ‘r implements a complete **"Fourth Tier"** compliance architecture natively, providing 20 production-ready primitives that seamlessly integrate into the execution graph: +LΓ‘r implements a complete **"Fourth Tier"** compliance architecture natively, providing production-ready primitives that seamlessly integrate into the execution graph: | Primitive | EU AI Act / Regulatory Match | Description | | :--- | :--- | :--- | | **`PolicyRegistry`** | Art. 9, 14 | Maps actions to risk tiers and determines oversight requirements. | | **`RiskScorerNode`** | Art. 14 | Pre-execution dynamic risk scoring and human-in-the-loop routing. | | **`RuntimeStateVersioner`** | Art. 3(23) | Detects "Substantial Modifications" (behavioral drift) during runtime. | -| **`CredentialVault`** | Art. 15(4) | Non-Human Identity (NHI) Just-in-Time privilege minimization. | +| **`CredentialVault`** | Art. 15(5) | Non-Human Identity (NHI) Just-in-Time privilege minimization. | | **`TransparencyEngine`** | Art. 13, 50 | Automated disclosure flagging for third-party interactions. | | **`PIIRedactionEngine`** | GDPR Art. 5, 17 | Right to Erasure: Cleans PII from states before cryptographic logging. | | **`AuditLogger` (Causal Trace)** | Art. 12 | Immutable State-Diff logs capturing explicit model reasoning traces. | | **`SyntheticMarkerNode`** | Art. 50(2) | Injects visible disclaimers or C2PA metadata into generated content. | -| **`BiasFilterNode`** | prEN 18283 | Evaluates state variables for bias heuristics before final output. | +| **`BiasFilterNode`** | Art. 10(2)(f)-(g); prEN 18283 (supporting) | Runtime bias keyword gate before final output β€” supports, does not by itself satisfy, prEN 18283 / Art. 10. | | **`PromptInjectionGuard`** | Art. 15(5) | Detects and blocks inputs designed to cause adversarial model failure. | | **`BranchTriageNode`** | Art. 14 (fractal) | Post-`BatchNode` primitive for parallel agents. Preserves per-dimension branch evidence before `ReduceNode` compression, ensuring the human jury sees individual branch findings β€” not only the consolidated score. Sets `branch_critical` for early-exit HITL routing. | | **`ComplianceManifestGenerator`** | Step 9, All | Statically walks the graph and auto-generates the exhaustive regulatory action inventory for auditors. | | **`LethalTrifectaGuard`** | GDPR Art. 5, Art. 14 | Runtime pre-execution guard enforcing the AEPD "Rule of 2" β€” blocks any action that combines untrusted input + sensitive data + autonomous effect without prior human approval. | | **`AuthorityLedger`** | Art. 12, 14 | The "Fourth Tier" β€” captures who exercised authority, their role, rationale, and risk score into a tamper-evident, HMAC-signed oversight record on every `HumanJuryNode` decision. | -| **`FundamentalRightsImpactNode`** | Art. 9 FRIA; EU Charter Arts. 1, 7/8, 11, 21, 47 | Runtime FRIA gate: scans text outputs across six EU Charter dimensions (dignity, privacy, non-discrimination, expression, justice, data protection) before proceeding. | -| **`ProhibitedPracticeGuard`** | Art. 5 | Detects prohibited AI practices at runtime: social scoring, subliminal manipulation, and exploitation of vulnerable groups. Raises `ProhibitedPracticeError` on a match. | -| **`IncidentReporter` + `IncidentReporterNode`** | Art. 72–74; ISO 9001 Cl. 9 | Real-time incident detection and structured JSONL logging with EU-mandated reporting deadlines (CRITICAL/HIGH: 24 h). `IncidentReporter` generates Post-Market Monitoring Markdown reports from audit logs. | -| **`MultiAgentBoundaryNode`** | Art. 3(1), Recital 12, Art. 25 | Records Art. 25 boundary classification (INTERNAL / EXTERNAL_MARKET) for every sub-agent call; warns when an external agent lacks a conformity ID. | +| **`FundamentalRightsImpactNode`** | Art. 9(2)(a) risk factor; EU Charter Arts. 1, 7/8, 11, 21, 47 | Runtime fundamental-rights *screen*: scans text outputs across six EU Charter dimensions before proceeding. **Not** the Art. 27 FRIA. | +| **`Article27FRIANode`** | Art. 27 β€” deployer Fundamental Rights Impact Assessment | Scope-gated on `deployer_class`; generates & completeness-checks the 27(1)(a)-(f) template; writes `fria_art27.md`. LΓ‘r produces the template, not the assessment. | +| **`ProhibitedPracticeGuard`** | Art. 5 | Detects prohibited-practice phrasing at runtime: social scoring, subliminal manipulation, exploitation of vulnerable groups, plus the 2 Dec 2026 Omnibus additions (NCII, AI-CSAM). Raises `ProhibitedPracticeError` on a match. | +| **`IncidentReporter` + `IncidentReporterNode`** | Art. 72–74; ISO 9001 Cl. 9 | Real-time incident detection and structured JSONL logging with a conservative 48h/360h deadline ceiling plus all three Art. 73 legal deadlines per record. `IncidentReporter` generates Post-Market Monitoring Markdown reports from audit logs. | +| **`MultiAgentBoundaryNode`** | Art. 3(1), Art. 25 | Records Art. 25 boundary classification (INTERNAL / EXTERNAL_MARKET) for every sub-agent call; warns when an external agent lacks a conformity ID. | | **`SupplierAgreementRegistry`** | Art. 25(4) | Maintains signed written agreements with tool suppliers. `assert_agreement()` blocks execution if an agreement is missing or expired. Exports a Markdown table for the compliance manifest. | | **`DynamicToolDiscoveryMonitor`** | Art. 3(23), Art. 9 | Compares the live tool catalogue against the conformity-assessed baseline. Sets `substantial_modification_flag` and optionally raises `UndisclosedToolError` when new tools appear. | | **`DeployerTransparencyNode`** | Art. 13, Annex IV | Generates a machine-readable Art. 13 instructions-for-use disclosure (intended purpose, known limitations, human oversight requirements, prohibited uses) per session. | @@ -66,9 +67,9 @@ LΓ‘r implements a complete **"Fourth Tier"** compliance architecture natively, p ## Enterprise Reference Implementation -> The canonical, working reference that ticks every compliance box the April 2026 EU AI Act research paper identifies. +> The canonical, working reference that maps each primitive to a provision of Regulation (EU) 2024/1689 (as amended); a mapped row = a runtime hook + evidence, not a conformity assessment. -LΓ‘r ships a **single reusable backbone** that wires the compliance primitives (20 in lar.compliance; the backbone's own docstring documents 23 EU AI Act requirement mappings) into an end-to-end auditable graph. Target any regulated vertical by supplying a domain name: +LΓ‘r ships a **single reusable backbone** that wires the compliance primitives (in lar.compliance) β€” 26 runtime rows into an end-to-end auditable graph. Target any regulated vertical by supplying a domain name: ```bash python src/lar/enterprise/run.py HEALTHCARE # MDR + EU AI Act + GDPR + FDA 21 CFR 11 @@ -119,7 +120,7 @@ python examples/compliance/22_eu_ai_act_finance_showcase.py ``` It explicitly validates: -1. **Article 15(4)**: JIT Privilege (CredentialVault) +1. **Article 15(5)**: JIT Privilege (CredentialVault) 2. **GDPR Article 17**: PII Redaction 3. **Article 12**: Causal Audit Logging 4. **Article 9 & 14**: Policy Registry & Risk Scoring @@ -412,12 +413,13 @@ guard = ProhibitedPracticeGuard(input_key="final_output", next_node=output_node) --- -## 7. Art. 9 FRIA: Fundamental Rights Impact Assessment -**Requirement**: Art. 9 requires providers to assess the impact of high-risk AI systems on fundamental rights as part of the risk management system. +## 7. Fundamental rights β€” two distinct obligations -**LΓ‘r Solution**: `FundamentalRightsImpactNode` +### 7a. Art. 9(2)(a) β€” runtime fundamental-rights screen (provider) -A runtime FRIA gate that scans text outputs across **six EU Charter dimensions** using configurable regex heuristics: +**Requirement**: Art. 9(2)(a) requires the risk management system to identify the risks a high-risk system can pose to health, safety **or fundamental rights**. + +**LΓ‘r Solution**: `FundamentalRightsImpactNode` β€” a runtime *screen* (not the named FRIA) that scans text outputs across **six EU Charter dimensions** using configurable regex heuristics: | Dimension | EU Charter Article | |:---|:---| @@ -441,10 +443,33 @@ fria = FundamentalRightsImpactNode( # Wire after every LLMNode or ToolNode that produces text touching a person. ``` +### 7b. Art. 27 β€” Fundamental Rights Impact Assessment (deployer) + +**Requirement**: Art. 27 requires *certain deployers* β€” public bodies, private providers of public services, and deployers doing credit scoring (Annex III 5(b)) or life & health insurance risk pricing (Annex III 5(c)) β€” to carry out a FRIA **before first use**, covering: (a) deployer processes; (b) period/frequency of use; (c) affected natural persons; (d) specific fundamental-rights risks (informed by the Art. 13 provider information); (e) human-oversight measures; (f) measures on materialisation incl. internal governance and complaint mechanisms. Once done, the deployer notifies the market surveillance authority (Art. 27(3)); it may cross-refer a GDPR Art. 35 DPIA (Art. 27(4), reinforced by the Digital Omnibus). + +**LΓ‘r Solution**: `Article27FRIANode` β€” scope-gated on `deployer_class`; generates the 27(1)(a)-(f) template, reports completeness, records the notification duty, and writes `enterprise_audit/fria_art27.md` via `as_markdown()`. LΓ‘r generates and checks the template; it does **not** perform the assessment. + +```python +from lar.compliance import Article27FRIANode + +fria27 = Article27FRIANode( + system_name="Credit Decision Agent", + deployer_class="CREDIT_SCORING", # or PUBLIC_BODY / PRIVATE_PUBLIC_SERVICE / LIFE_HEALTH_INSURANCE + annex_iii_point="5(b)", + deployment_process="...", usage_period_and_frequency="...", + affected_natural_persons=["applicants", "guarantors"], + fundamental_rights_risks=["proxy bias (Charter Art. 21)"], + human_oversight_measures=["mandatory reviewer approval gate"], + measures_on_materialisation=["suspend + notify authority", "complaint channel"], + dpia_reference="DPIA-2026-014", + strict=True, # raise if any 27(1) element is missing +) +``` + --- -## 8. Art. 72–74: Serious Incident Reporting -**Requirement**: Art. 73–74 require providers to report serious incidents to national authorities within defined deadlines (typically 24 hours for life/safety risks). +## 8. Art. 72–73: Post-market monitoring & serious incident reporting +**Requirement**: Art. 73 requires providers to report serious incidents promptly. The deadlines are keyed to incident **type**, not a severity score: Art. 73(2) general default **15 days**; Art. 73(3) widespread infringement / serious incident per Art. 3(49)(b) **2 days**; Art. 73(4) death of a person **10 days**. **LΓ‘r Solution**: `IncidentReporterNode` + `IncidentReporter` @@ -453,15 +478,17 @@ fria = FundamentalRightsImpactNode( 1. **Graph node** β€” scans the live `GraphState` for harm signal keys (`_prohibited_practice_flag`, `_trifecta_check`, `fria_findings`, `bias_detected`, `last_error`) and writes structured incident records. 2. **Executor hook** β€” `report_runtime_error()` is called by `GraphExecutor` on unhandled exceptions; classifies and records automatically. -Every record written to `.jsonl` includes the EU article reference, severity, and a `reporting_deadline_hours` field: +Every record written to `.jsonl` includes the EU article reference, a **conservative deadline ceiling** in `reporting_deadline_hours`, the full `art_73_legal_deadlines` map, and `provider_must_confirm_applicable_paragraph: true` β€” because LΓ‘r cannot infer from a generic harm signal whether an incident is legally "widespread" or involved a death: -| Severity | Trigger | Deadline | +| LΓ‘r severity | Trigger | Conservative ceiling | |:---|:---|:---| -| CRITICAL | `ProhibitedPracticeError`, `LethalTrifectaError`, `FRIAViolation` | 24 h | -| HIGH | `SecurityError`, `AgreementNotFoundError`, `UndisclosedToolError` | 24 h | -| MEDIUM | State contains trifecta/prohibited flags | 72 h | +| CRITICAL | `ProhibitedPracticeError`, `LethalTrifectaError`, `FRIAViolation` | 48 h (Art. 73(3) fastest deadline, as a fail-safe) | +| HIGH | `SecurityError`, `AgreementNotFoundError`, `UndisclosedToolError` | 48 h (same fail-safe β€” **not** an Art. 73(4) death classification) | +| MEDIUM | State contains trifecta/prohibited flags | 360 h (Art. 73(2) general default β€” 15 days) | + +The provider must confirm the applicable Art. 73 paragraph against the incident facts. -**`IncidentReporter`** aggregates run logs and the `AuthorityLedger` post-execution to generate a Post-Market Monitoring (PMM) Markdown report (Art. 72 / ISO 9001 Cl. 9), including rejection rate alerts and drift severity summaries. +**`IncidentReporter`** aggregates run logs and the `AuthorityLedger` post-execution to generate a Post-Market Monitoring (PMM) evidence summary β€” it **supports** Art. 72 but does not replace the documented post-market monitoring plan (Art. 72(3)). --- @@ -568,8 +595,9 @@ art13 = DeployerTransparencyNode( | **Trifecta Guard** | Runtime AEPD enforcement | Meets GDPR Art. 5, Art. 14 (AEPD Rule of 2). | | **Authority Records** | `AuthorityLedger` | Closes the "Fourth Tier" gap β€” Art. 12/14 action-level evidence chain. | | **Prohibited Practices** | `ProhibitedPracticeGuard` | Meets Art. 5 β€” blocks social scoring, manipulation, vulnerability exploitation at runtime. | -| **Fundamental Rights** | `FundamentalRightsImpactNode` | Meets Art. 9 FRIA β€” six EU Charter dimensions scanned per output. | -| **Incident Reporting** | `IncidentReporterNode` + `IncidentReporter` | Meets Art. 72–74 β€” structured JSONL with 24 h deadline flags; PMM Markdown reports. | +| **Fundamental Rights (screen)** | `FundamentalRightsImpactNode` | Art. 9(2)(a) runtime screen β€” six EU Charter dimensions per output (not the Art. 27 FRIA). | +| **Fundamental Rights (deployer FRIA)** | `Article27FRIANode` | Art. 27 β€” generates & completeness-checks the deployer FRIA template; writes `fria_art27.md`. | +| **Incident Reporting** | `IncidentReporterNode` + `IncidentReporter` | Supports Art. 72–73 β€” structured JSONL with a conservative deadline ceiling + all three Art. 73 legal deadlines; PMM evidence summary. | | **Multi-Agent Boundaries** | `MultiAgentBoundaryNode` | Meets Art. 25 β€” INTERNAL/EXTERNAL_MARKET classification per sub-agent call. | | **Supplier Agreements** | `SupplierAgreementRegistry` | Meets Art. 25(4) β€” written agreement enforcement; blocks on missing/expired. | | **Tool Discovery** | `DynamicToolDiscoveryMonitor` | Meets Art. 3(23) β€” flags substantial modification when undisclosed tools appear. | diff --git a/docs/compliance/annex-iv-template.md b/docs/compliance/annex-iv-template.md index d86a34d..3cffc88 100644 --- a/docs/compliance/annex-iv-template.md +++ b/docs/compliance/annex-iv-template.md @@ -3,7 +3,9 @@ > **Instructions for the Provider (You):** > Under the EU AI Act (Article 11), the legal "Provider" (the organisation placing the High-Risk AI System on the market) must draw up technical documentation in accordance with Annex IV *before* conformity assessment. > -> **LΓ‘r generates the architectural and action-inventory evidence for this document automatically.** You must fill in the business, data, and testing details. +> **LΓ‘r auto-generates the action-inventory section (Β§2 below β€” Annex IV Β§2(b), components and their interactions) and parts of Β§4–6 (oversight records, causal trace).** Everything else β€” provider identity, intended purpose, data governance, training data, testing results, risk-management documentation, the conformity assessment itself β€” is yours to complete. LΓ‘r produces evidence; it does not produce the technical documentation. +> +> Separately, if the **deployer** is a public body, a private provider of public services, or a credit-scoring / life-&-health-insurance risk deployer, they owe an **Article 27 Fundamental Rights Impact Assessment** β€” `Article27FRIANode` generates that template (`fria_art27.md`); it is a deployer document, not part of this provider technical file. --- diff --git a/docs/compliance/auditor_guide.md b/docs/compliance/auditor_guide.md index b2417cc..9202993 100644 --- a/docs/compliance/auditor_guide.md +++ b/docs/compliance/auditor_guide.md @@ -15,9 +15,11 @@ For the canonical reference implementation showing the compliance primitives fir --- -## 2. The Three Required Artefacts +## 2. The Required Artefacts -Request these from the engineering team for any conformity assessment or incident investigation. Every LΓ‘r Enterprise run writes all three to `enterprise_audit/`. +Request these from the engineering team for any conformity assessment or incident investigation. Every LΓ‘r Enterprise run writes them to `enterprise_audit/`. A fourth β€” `fria_art27.md` β€” appears only when the deployer is in scope for Article 27 (see Β§2.4). + +> **Timeline (post Digital Omnibus, Regulation (EU) 2026/1744).** Standalone Annex III high-risk obligations apply from **2 December 2027** (deferred from 2 Aug 2026); Annex I embedded high-risk from 2 Aug 2028. Article 5 (since Feb 2025; NCII/CSAM from 2 Dec 2026), GPAI (since Aug 2025) and Article 50 (since Aug 2026; 2 Dec 2026 grace for 50(2) marking) are unaffected. An audit before Dec 2027 is a readiness audit, not an enforcement one. ### Artefact 1 β€” Action Inventory (`compliance_manifest.json`) *Regulatory basis: Step 9 (Nannini et al.), Annex IV* @@ -27,7 +29,7 @@ Generated by static graph traversal **before** the agent runs. This is the regul **What to verify:** - **External actions**: Every tool, LLM call, and API endpoint the agent can reach. If an action appears at runtime that is not in the manifest, that is an Art. 3(23) Substantial Modification event. - **Affected parties**: Actions flagged `THIRD_PARTY` must have Art. 50 disclosure evidence in the causal trace. -- **Unvaulted tools**: `tools_without_credential_vault` must be zero in production. Any tool not using `CredentialVault` holds standing credentials β€” a Art. 15(4) violation. +- **Unvaulted tools**: `tools_without_credential_vault` must be zero in production. Any tool not using `CredentialVault` holds standing credentials β€” a Art. 15(5) violation. - **Risk flags**: `HIGH` severity flags (e.g., `AdaptiveNode` present) require explicit provider documentation before CE-marking submission. ### Artefact 2 β€” Causal Trace (`run_.json`) @@ -52,6 +54,19 @@ Records every human oversight exercise. This is the evidentiary chain the paper' - **Rationale**: Every record must contain a non-empty `rationale`. An approval with no rationale is rubber-stamping β€” it will fail an audit for negligent oversight. - **Timestamp realism**: Cross-reference `timestamp` against the causal trace. If a `HumanJuryNode` decision appears in under 10 seconds on a complex case, investigate. - **Risk score**: The `risk_score_key` field shows the upstream `RiskScorerNode` output that triggered the interrupt. Verify the score matches the action's `PolicyRegistry` classification. +- **Automation boundary**: For a high-risk decision the `HumanJuryNode` must be configured `automation_boundary="always_human"`. Confirm the recorded decision came from a real reviewer (interactive, or a `human_decision_provider` wired to a genuine reviewer surface) β€” **not** from an `auto_first_choice` fallback. A run that produced a ledger record with no reachable human is a finding. + +### Artefact 4 β€” Article 27 FRIA (`fria_art27.md` / `fria_art27.json`) +*Regulatory basis: Art. 27 β€” Fundamental Rights Impact Assessment (deployer obligation)* + +Present only when `deployer_class` is in scope (public body; private provider of public services; credit scoring β€” Annex III 5(b); or life & health insurance risk pricing β€” Annex III 5(c)). + +**What to verify:** +- **Scope call is correct**: if the deployer is one of the four in-scope classes, `applicable` must be `true`. An `applicable: false` record for an in-scope deployer is a misclassification. +- **All six elements present**: `completeness.complete` must be `true` β€” 27(1)(a) processes, (b) period/frequency, (c) affected persons, (d) specific fundamental-rights risks (informed by the Art. 13 provider information), (e) human-oversight measures, (f) measures on materialisation incl. internal governance and complaint mechanisms. +- **Authority notification**: `notify_market_surveillance_authority.required` is `true` β€” confirm the deployer actually filed the AI Office template (Art. 27(3)/(5)). +- **DPIA link**: `dpia_reference_art27_4` should point to the GDPR Art. 35 DPIA the FRIA cross-refers (Art. 27(4), reinforced by the Digital Omnibus). +- LΓ‘r generates the template and checks completeness β€” it does **not** perform the assessment. The substantive content is the deployer's. --- @@ -86,7 +101,7 @@ Use this table to verify which steps the agent's architecture addresses. Request | 4 | Risk management: Art. 9 | `PolicyRegistry` + `RiskScorerNode` | `computed_oversight_level` in causal trace | | 5 | Data governance: prEN 18283/18284 | `PIIRedactionEngine` + `BiasFilterNode` | PII absent from log; bias scan in state diff | | 6 | Trustworthiness: Art. 12–14 | `AuditLogger` + `HumanJuryNode` + `AuthorityLedger` | All three artefacts | -| 7 | Cybersecurity: Art. 15(4) | `CredentialVault` | `jit_token_present` in causal trace; unvaulted tools = 0 in manifest | +| 7 | Cybersecurity: Art. 15(5) | `CredentialVault` | `jit_token_present` in causal trace; unvaulted tools = 0 in manifest | | 8 | CRA applicability | Secure-by-design architecture | Provider documentation | | 9 | Adjacent legislation inventory | `ComplianceManifestGenerator` | Compliance manifest action inventory | | 10 | Conformity assessment artefacts | Manifest + Ledger + Trace β†’ Annex IV | All three artefacts | @@ -110,6 +125,9 @@ Use this table to verify which steps the agent's architecture addresses. Request | **Tampered log** | HMAC verification fails | HMAC-SHA256 signing | | **Bias in output** | Protected characteristic in LLM output with no interrupt | `BiasFilterNode` + `HumanJuryNode` | | **Consolidated-only jury context** | `BatchNode` present but no `branch_findings_summary` in `HumanJuryNode` context keys | `BranchTriageNode` | +| **Auto-approved high-risk decision** | Ledger record for a high-risk decision but `automation_boundary` not `always_human`, or no reachable reviewer surface | `HumanJuryNode(automation_boundary="always_human")` + `human_decision_provider` | +| **Missing Art. 27 FRIA** | In-scope `deployer_class` but no `fria_art27.md`, or `applicable: false`, or `completeness.complete: false` | `Article27FRIANode` | +| **Overclaimed bias/data-governance conformance** | `BiasFilterNode` cited as satisfying prEN 18283 / Art. 10 rather than as a keyword aid | scope the claim to Art. 10(2)(f)-(g) examination support | --- @@ -118,8 +136,10 @@ Use this table to verify which steps the agent's architecture addresses. Request 1. What is the `conformity_id` for this system and where is the conformity assessment record? 2. Where is the HMAC secret stored and who has access to it? 3. Has the `compliance_manifest.json` been reviewed and signed off before production deployment? -4. Are `HumanJuryNode` approvers trained on what they are approving, or is rubber-stamping structurally possible? +4. Are `HumanJuryNode` approvers trained on what they are approving? Is `automation_boundary="always_human"` set for every high-risk decision type, and what real surface delivers the `human_decision_provider` decision (web form, Slack, on-call reviewer)? 5. Is `RuntimeStateVersioner` configured with a conformity baseline, and what is the drift threshold before a new assessment is triggered? +6. Is the deployer in scope for Article 27? If so, where is the completed `fria_art27` and the record that it was filed with the market surveillance authority (Art. 27(3))? +7. Which Art. 25(4) written agreements exist with non-FOSS suppliers (model gateways, data services, external APIs)? LΓ‘r itself (Apache-2.0, non-GPAI) is outside 25(4). --- diff --git a/docs/compliance/enterprise-reference.md b/docs/compliance/enterprise-reference.md index e63320c..bd8fe16 100644 --- a/docs/compliance/enterprise-reference.md +++ b/docs/compliance/enterprise-reference.md @@ -1,6 +1,6 @@ # Enterprise Reference Implementation -> **v2.2.0** β€” The canonical, working reference that ticks all **23 compliance boxes** from the April 2026 EU AI Act research paper. Validated live with `ollama/phi4:latest` β€” no API key required. +> The canonical working reference for the EU AI Act compliance backbone: **26 runtime rows** mapped to Regulation (EU) 2024/1689 (as amended by the Digital Omnibus, Regulation (EU) 2026/1744). Validated live with `ollama/phi4:latest` (override with `LAR_SHOWCASE_MODEL`) β€” no API key required. "Ticks a box" = a runtime hook fires and/or an evidence artifact is produced; it is **not** a conformity assessment. **Files:** @@ -20,10 +20,10 @@ If you need to prove compliance to an auditor or understand how the compliance p python examples/compliance/22_eu_ai_act_finance_showcase.py ``` -It explicitly validates all 23 requirements from Nannini et al. (2026): +It explicitly validates 26 runtime rows: **Original 12 steps:** -1. **Art. 15(4)**: JIT + trust-based privilege (`CredentialVault.get_with_trust()`) +1. **Art. 15(5)** (cybersecurity β€” NHI least privilege): JIT + trust-based privilege (`CredentialVault.get_with_trust()`) 2. **GDPR Art. 17**: PII redaction + erasable per-subject memory (`SessionMemoryNode`) 3. **Art. 12**: Causal audit logging + per-step integrity (`verify_step_integrity()`) 4. **Art. 9 & 14**: PolicyRegistry + RiskScorerNode @@ -34,20 +34,23 @@ It explicitly validates all 23 requirements from Nannini et al. (2026): 9. **Step 9**: ComplianceManifestGenerator (auto-detects adjacent legislation) 10. **prEN 18283**: BiasFilterNode -**v2.2.0 gap-closure (A–L):** -11. **Art. 9 FRIA**: FundamentalRightsImpactNode (6 EU Charter dimensions) -12. **Art. 9 PMM**: BehavioralEnvelopeMonitor (output variance vs. baseline) -13. **Art. 12 depth**: `log_plan_switch()` for RouterNode branch-switch events +**Gap-closure rows (A–O):** +11. **Art. 9(2)(a)** runtime fundamental-rights screening: FundamentalRightsImpactNode (6 EU Charter dimensions) β€” *not* the Art. 27 FRIA +12. **Art. 9(9) PMM**: BehavioralEnvelopeMonitor (output variance vs. baseline) +13. **Art. 12 depth**: `log_plan_switch()` for branch-switch events 14. **Art. 13 deployer**: DeployerTransparencyNode (instructions for use) -15. **Art. 14 boundary**: `automation_boundary` + `decision_type` on HumanJuryNode -16. **Art. 25(4)**: SupplierAgreementRegistry (written agreement enforcement) +15. **Art. 14 boundary**: `automation_boundary="always_human"` + a real out-of-band decision via `human_decision_provider` (no auto-approve) +16. **Art. 25(4)**: SupplierAgreementRegistry (written agreement enforcement; FOSS carve-out β€” LΓ‘r itself is outside 25(4)) 17. **Art. 3(23) tools**: DynamicToolDiscoveryMonitor (post-conformity additions) 18. **Art. 3 boundary**: MultiAgentBoundaryNode (INTERNAL vs. EXTERNAL_MARKET) -19. **Art. 73–74**: IncidentReporterNode (executor hook β€” auto-fires on exceptions) +19. **Art. 73**: IncidentReporterNode (executor hook; conservative 48h/360h ceiling + all three Art. 73 legal deadlines per record) 20. **GDPR Art. 17 memory**: SessionMemoryNode (write/read/erase/audit modes) -21. **Art. 15(4) trust**: `get_with_trust()` β€” trust-level-gated credential access -22. **Art. 5 auto**: ProhibitedPracticeGuard executor hook (fires on every LLM output) -23. **Art. 50(2)**: SyntheticMarkerNode (AI content marking) +21. **Art. 15(5) trust**: `get_with_trust()` β€” trust-level-gated credential access +22. **Art. 5 auto**: ProhibitedPracticeGuard executor hook (incl. the 2 Dec 2026 Omnibus NCII/CSAM additions) +23. **Art. 50(2)**: SyntheticMarkerNode METADATA (machine-readable marking) +24. **Art. 50(1)/(4)**: SyntheticMarkerNode VISIBLE (human-readable disclosure / deep-fake label) +25. **Art. 27**: Article27FRIANode β€” the deployer Fundamental Rights Impact Assessment (scope-gated; writes `fria_art27.md`) +26. **Art. 11 / Annex IV Β§2(b)**: ComplianceManifestGenerator feeds the technical-documentation components inventory --- @@ -145,7 +148,7 @@ The `PIIRedactionEngine` strips all configured PII keys **before** the HMAC sign --- -### Problem 2 β€” Art. 15(4): Static Credentials = Catastrophic Attack Surface +### Problem 2 β€” Art. 15(5): Static Credentials = Catastrophic Attack Surface > *"Systems must enforce the principle of least privilege. Providing an agent with static, high-level API keys creates a catastrophic attack surface for prompt injection or autonomous drift."* @@ -248,7 +251,7 @@ This is saved as a separate HMAC-signed `authority_ledger.json` β€” the action-l ## The Execution Order: A Compliance Walkthrough ``` -[A] CredentialVault β†’ Art. 15(4): JIT NHI token, no static credentials +[A] CredentialVault β†’ Art. 15(5): JIT NHI token, no static credentials [B] LLMNode β†’ domain-aware analysis of the case [C] FunctionalNode (parse) β†’ extract risk_level, recommendation, confidence [D] RiskScorerNode β†’ Art. 14: route to HumanJuryNode if PRE_EXECUTION diff --git a/docs/compliance/eu-ai-act-deep-dive.md b/docs/compliance/eu-ai-act-deep-dive.md index f8cfe00..d74effd 100644 --- a/docs/compliance/eu-ai-act-deep-dive.md +++ b/docs/compliance/eu-ai-act-deep-dive.md @@ -9,7 +9,7 @@ LΓ‘r was explicitly re-engineered to solve the unique, agent-specific legal chal ## 1. Action-Chain Causal Auditability (Art. 12) **The Problem:** -The EU AI Act's logging requirements (Article 12, operationalized via prEN ISO/IEC 24970) demand sufficient traceability. The paper notes: *"logging must capture not only each individual step but the causal relationships between them: why did the agent select this tool rather than that one? ... none produce audit trails that meet Article 12’s requirement."* Furthermore, because agents can hallucinate or misreport their actions, the audit must independently verify the system state. +The EU AI Act's logging requirements (Article 12, operationalized via prEN ISO/IEC DIS 24970) demand sufficient traceability. The paper notes: *"logging must capture not only each individual step but the causal relationships between them: why did the agent select this tool rather than that one? ... none produce audit trails that meet Article 12’s requirement."* Furthermore, because agents can hallucinate or misreport their actions, the audit must independently verify the system state. **The LΓ‘r Solution: Causal Trace Logging** @@ -42,10 +42,12 @@ The EU AI Act's logging requirements (Article 12, operationalized via prEN ISO/I **HMAC-SHA256 signature of full log:** `55931245a2c8117f1c1dc4f6b4499b866f272d99bd9273cd01d313e435a658a5` -## 2. Privilege Minimisation Outside the Model (Art. 15(4)) +## 2. Privilege Minimisation Outside the Model (Art. 15(5)) + +> **Citation note.** In Regulation (EU) 2024/1689 as enacted, **Art. 15(5)** is the cybersecurity paragraph ("resilient against attempts by unauthorised third parties … data poisoning … adversarial examples"); Art. 15(4) is robustness / error-resilience / feedback loops. Least privilege for tool credentials is not verbatim 15(5) text β€” it is a proportionate technical measure that *supports* 15(5), aligned with **prEN 18282** (itself scoped to Art. 15(5)) and the Cyber Resilience Act's least-privilege principle. Earlier revisions of this page cited "15(4)"; corrected repo-wide in the 2026-09 conformance pass. **The Problem:** -Agents interact with the world via "tools". Under the AI Cyber Resilience standard (prEN 18282), systems must enforce the principle of least privilege. Providing an agent with static, high-level API keys creates a catastrophic attack surface for prompt injection or autonomous drift. +Agents interact with the world via "tools". Under the AI cybersecurity standard (prEN 18282), systems should enforce the principle of least privilege. Providing an agent with static, high-level API keys creates a catastrophic attack surface for prompt injection or autonomous drift. **The LΓ‘r Solution: The Credential Vault** LΓ‘r implements a `CredentialVault` directly inside the `ToolNode`. @@ -321,3 +323,24 @@ Every human decision now produces an `AuthorityRecord` containing: The ledger is then saved as a HMAC-SHA256 signed JSON file alongside the `AuditLogger`'s Causal Trace β€” completing the full evidence chain from **action proposal β†’ risk assessment β†’ human determination β†’ execution outcome** required by Articles 12–14. **See the full example:** `examples/compliance/21_authority_and_trifecta.py` + +--- + +## 11. The Deployer's Fundamental Rights Impact Assessment (Art. 27) + +**The distinction:** Β§7 above (`FundamentalRightsImpactNode`) is a *runtime screen* mapped to Art. 9(2)(a) β€” a provider-side risk-management input. It is **not** the Act's named FRIA. The **Article 27** Fundamental Rights Impact Assessment is a **deployer** obligation, owed by (i) public bodies, (ii) private providers of public services, and (iii) deployers doing credit scoring (Annex III 5(b)) or life & health insurance risk pricing (Annex III 5(c)) β€” before first use. + +**The LΓ‘r Solution: `Article27FRIANode`** + +Scope-gated on `deployer_class`. For an in-scope deployer it generates the Art. 27(1)(a)–(f) template β€” deployer processes; period and frequency of use; categories of affected natural persons; the specific fundamental-rights risks (informed by the Art. 13 provider information); the human-oversight measures; and the measures on materialisation, including internal governance and complaint mechanisms β€” reports completeness, records the Art. 27(3) authority-notification duty, and carries an Art. 27(4) DPIA cross-reference (reinforced by the Digital Omnibus). `as_markdown()` produces the filing-ready document; the enterprise backbone writes `enterprise_audit/fria_art27.md`. + +LΓ‘r generates and completeness-checks the template. It does not perform the assessment β€” the deployer supplies the substantive content and files it with the market surveillance authority. + +--- + +## 12. Digital Omnibus (Regulation (EU) 2026/1744) β€” what changed + +- **Annex III high-risk obligations deferred** to **2 December 2027** (Annex I embedded high-risk to 2 Aug 2028). This page's Art. 12–15 machinery is what applies from that date. +- **Two new Art. 5 prohibitions** β€” non-consensual intimate imagery and AI-generated CSAM β€” from **2 December 2026**. `ProhibitedPracticeGuard` gains `NCII` / `CSAM` heuristic categories. +- **Art. 27(4)** β€” a FRIA may incorporate or cross-refer a GDPR Art. 35 DPIA; **Annex VIII registration simplified**; **SME / small-mid-cap relief** (lighter technical documentation, proportionate QMS, cybersecurity presumption of conformity). +- **Art. 50 unchanged** β€” applies from 2 Aug 2026; 2 Dec 2026 grace for the 50(2) machine-readable marking of generative systems already on the market. `SyntheticMarkerNode` METADATA = 50(2); VISIBLE = 50(1)/(4). diff --git a/docs/compliance/finance-showcase.md b/docs/compliance/finance-showcase.md index 0ce62f6..d7927a2 100644 --- a/docs/compliance/finance-showcase.md +++ b/docs/compliance/finance-showcase.md @@ -1,6 +1,6 @@ # EU AI Act Finance Showcase -> **v2.2.0** β€” One command runs a live SME credit decision through all **23 requirements** from Nannini et al. (2026) using `ollama/phi4:latest` (fully local, no API key needed) and produces three HMAC-signed audit artefacts. LΓ‘r ships **20 compliance primitives** β€” all open-source. +> One command runs a live SME credit decision through **26 runtime rows** mapped to Regulation (EU) 2024/1689 (as amended by the Digital Omnibus, Regulation (EU) 2026/1744), using `ollama/phi4:latest` by default (override with `LAR_SHOWCASE_MODEL`), and produces four artefacts: an HMAC-signed causal trace, an authority ledger, an action-inventory manifest, and the deployer's Art. 27 Fundamental Rights Impact Assessment (`fria_art27.md`). "Row covered" = a runtime hook fires and/or an evidence artifact is produced β€” **not** a conformity assessment. All primitives are open-source. ```bash python examples/compliance/22_eu_ai_act_finance_showcase.py @@ -8,24 +8,26 @@ python examples/compliance/22_eu_ai_act_finance_showcase.py **Source:** [`examples/compliance/22_eu_ai_act_finance_showcase.py`](https://github.com/snath-ai/lar/blob/main/examples/compliance/22_eu_ai_act_finance_showcase.py) -Any model supported by LiteLLM works. To switch models, set `BACKBONE_MODEL`: +Any model supported by LiteLLM works. To switch models, set `LAR_SHOWCASE_MODEL`: ```bash -# Local (default) +# Local (default: ollama/phi4:latest) python examples/compliance/22_eu_ai_act_finance_showcase.py +# Smaller / faster local model +LAR_SHOWCASE_MODEL=ollama/llama3.2:1b python examples/compliance/22_eu_ai_act_finance_showcase.py + # Cloud -BACKBONE_MODEL=gpt-4o python examples/compliance/22_eu_ai_act_finance_showcase.py -BACKBONE_MODEL=gemini/gemini-1.5-pro python examples/compliance/22_eu_ai_act_finance_showcase.py +LAR_SHOWCASE_MODEL=gpt-4o python examples/compliance/22_eu_ai_act_finance_showcase.py ``` --- -## What It Validates (v2.2.0 β€” 23 Requirements) +## What It Validates (26 runtime rows) -The showcase runs a high-risk credit application (Annex III, point 5(b) β€” creditworthiness assessment) through every compliance primitive in sequence, then verifies the three output artefacts against their regulatory obligations. +The showcase runs a high-risk credit application (Annex III, point 5(b) β€” creditworthiness assessment) through every compliance primitive in sequence, then verifies the output artefacts against their regulatory obligations. FINANCE / credit scoring means the deployer owes an **Art. 27 FRIA** β€” the showcase generates and completeness-checks it (`fria_art27.md`). -**Original 12 steps (v2.1.x)** +**Original 12 steps** | # | Primitive | Article / Standard | Validated | |:---|:---|:---|:---| @@ -34,28 +36,31 @@ The showcase runs a high-risk credit application (Annex III, point 5(b) β€” cred | S4 | `PolicyRegistry` + `RiskScorerNode` | Art. 9 + Art. 14 β€” risk management | `computed_oversight_level` written to state | | S5 | `PIIRedactionEngine` + `BiasFilterNode` | prEN 18284/18283 / Art. 10 | SSN + Name stripped; bias scan on LLM output | | S6 | `AuditLogger` + `HumanJuryNode` + `AuthorityLedger` | Art. 12–14 β€” trustworthiness | HMAC trace + structural HITL + Fourth Tier record | -| S7 | `CredentialVault` | prEN 18282 / Art. 15(4) | JIT NHI provisioning β€” agent holds no standing credentials | +| S7 | `CredentialVault` | prEN 18282 / Art. 15(5) | JIT NHI provisioning β€” agent holds no standing credentials | | S8 | Secure-by-design architecture | CRA Annex I | Credential minimisation + HMAC signing | | S9 | `ComplianceManifestGenerator` | Step 9 β€” adjacent legislation | DORA, MiFID II, GDPR auto-detected from domain | | S10 | Manifest + Ledger + Causal Trace | Annex IV | Three Annex IV documentation inputs signed | | S11 | `RuntimeStateVersioner` | Art. 3(23) | Drift report against conformity baseline | -**v2.2.0 gap-closure (rows A–L)** +**Gap-closure rows A–O** | # | Primitive | Article / Standard | Validated | |:---|:---|:---|:---| -| A | `FundamentalRightsImpactNode` | Art. 9 FRIA β€” 6 EU Charter dimensions | `fria_passed = True` after LLM output scan | -| B | `BehavioralEnvelopeMonitor` | Art. 9 PMM β€” output variance | Confidence score checked against baseline envelope | +| A | `FundamentalRightsImpactNode` | Art. 9(2)(a) β€” runtime FR screen (**not** the Art. 27 FRIA) | `fria_passed` written after LLM output scan | +| B | `BehavioralEnvelopeMonitor` | Art. 9(9) PMM β€” output variance | Confidence score checked against baseline envelope | | C | `AuditLogger.verify_step_integrity()` | Art. 12 β€” per-step integrity | State diff recomputed; MISMATCH β†’ tamper alert | | D | `AuditLogger.log_plan_switch()` | Art. 12 β€” causal chain depth | Branch-switch events in trace | | E | `DeployerTransparencyNode` | Art. 13 β€” instructions for use | Structured disclosure β†’ `state["deployer_instructions"]` | -| F | `HumanJuryNode(automation_boundary=…)` | Art. 14 β€” automation boundary | Per-decision-type policy (`always_human` / `auto_if_low_risk`) | -| G | `SupplierAgreementRegistry` | Art. 25(4) β€” written agreements | `assert_agreement(tool_name)` before every external call | +| F | `HumanJuryNode(automation_boundary="always_human")` | Art. 14 β€” automation boundary | Halts with no reviewer + no TTY; a real decision arrives via `human_decision_provider` and is signed into the AuthorityLedger with a rationale | +| G | `SupplierAgreementRegistry` | Art. 25(4) β€” written agreements (FOSS carve-out noted) | `assert_agreement(tool_name)` before every external call | | H | `DynamicToolDiscoveryMonitor` | Art. 3(23) β€” post-conformity tool addition | Flags tools added since baseline | | I | `MultiAgentBoundaryNode` | Art. 3 β€” sub-agent boundaries | `INTERNAL` vs `EXTERNAL_MARKET` declaration per sub-agent | -| J | `IncidentReporterNode` (executor hook) | Art. 73–74 β€” incident reporting | Auto-fires on unhandled exceptions; 24h/72h deadlines in record | +| J | `IncidentReporterNode` (executor hook) | Art. 73 β€” incident reporting | Conservative 48h/360h ceiling + all three Art. 73 legal deadlines + `provider_must_confirm_applicable_paragraph` per record | | K | `SessionMemoryNode` | GDPR Art. 17 β€” right to erasure | Per-subject compartment; `erase` mode deletes on request | -| L | `CredentialVault.get_with_trust()` | Art. 15(4) β€” trust-based privilege | Sensitive credentials blocked until trust_level="HIGH" | +| L | `CredentialVault.get_with_trust()` | Art. 15(5) β€” cybersecurity / trust-gated privilege | Sensitive credentials blocked until trust_level="HIGH" | +| M | `Article27FRIANode` | Art. 27 β€” **deployer** Fundamental Rights Impact Assessment | Scope-gated on `deployer_class`; generates & completeness-checks the 27(1)(a)-(f) template; writes `fria_art27.md` | +| N | `ProhibitedPracticeGuard` | Art. 5 (+ 2 Dec 2026 Omnibus) | Heuristic scan incl. `NCII` / `CSAM` categories | +| O | `SyntheticMarkerNode` Γ—2 | Art. 50(2) machine-readable + Art. 50(1)/(4) visible | Both markings applied to the final output | --- @@ -159,7 +164,7 @@ The paper (Section 8.1) defines a 12-step conformity assessment sequence. Here i | **4** | Risk management: prEN 18228 / Art. 9 | `PolicyRegistry` + `RiskScorerNode` | Runtime | | **5** | Data governance: prEN 18284 + prEN 18283 | `PIIRedactionEngine` + `BiasFilterNode` | Runtime | | **6** | Trustworthiness: Art. 12–14 | `AuditLogger` + `HumanJuryNode` + `AuthorityLedger` | Runtime | -| **7** | Cybersecurity: prEN 18282 / Art. 15(4) | `CredentialVault` (JIT NHI) | Runtime | +| **7** | Cybersecurity: prEN 18282 / Art. 15(5) | `CredentialVault` (JIT NHI) | Runtime | | **8** | CRA applicability | Secure-by-design architecture | Docs | | **9** | Adjacent legislation inventory | `ComplianceManifestGenerator` | Runtime | | **10** | Conformity assessment artefacts | Manifest + Ledger + Trace β†’ Annex IV | Artefacts | diff --git a/docs/compliance/paper-compliance-mapping.md b/docs/compliance/paper-compliance-mapping.md index 7134227..0b2636d 100644 --- a/docs/compliance/paper-compliance-mapping.md +++ b/docs/compliance/paper-compliance-mapping.md @@ -1,10 +1,14 @@ # LΓ‘r ↔ Nannini et al. (2026) Compliance Architecture Mapping -This page maps every compliance requirement from **"AI Agents Under EU Law: A Compliance Architecture for AI Providers"** (Nannini, Smith, Maggini et al., April 2026, arXiv:2604.04604v1) to the specific LΓ‘r primitive that implements it. +This page maps each compliance requirement from **"AI Agents Under EU Law: A Compliance Architecture for AI Providers"** (Nannini, Smith, Maggini et al., April 2026, arXiv:2604.04604v1) β€” a secondary source β€” to the LΓ‘r primitive that addresses it. Article citations are verified against **Regulation (EU) 2024/1689** as amended by the **Digital Omnibus, Regulation (EU) 2026/1744** (in force 27 July 2026). -The paper proposes a 12-step compliance sequence (Section 8.1) and identifies four agent-specific challenges (Section 6). LΓ‘r's Enterprise Compliance Backbone **v2.2.0** addresses every step, every challenge, and the 12 additional gap items identified by the audit β€” **23 requirements total, all fully covered**. +The paper proposes a 12-step compliance sequence (Section 8.1) and identifies four agent-specific challenges (Section 6). LΓ‘r's Enterprise Compliance Backbone addresses every step, every challenge, and 13 additional gap items β€” **26 rows**. -> **v2.2.0 (May 2026):** Gap-closure release. Added 7 new nodes, hardened the executor with automatic Art. 5 + Art. 73 hooks, and extended 6 existing primitives. See the [v2.2.0 gap-closure section](#v220-gap-closure-requirements) below. +> **What "covered" means.** A row is covered when a runtime hook fires and/or an evidence artifact is produced. It does **not** mean the underlying legal obligation is discharged. Conformity assessment (Art. 43), the QMS (Art. 17), the EU declaration of conformity (Art. 47), EU-database registration (Art. 49), and the substantive content of every assessment remain the customer-provider's and the deployer's responsibility. LΓ‘r is a component in the Art. 25 value chain, not the provider. + +> **v2.2.0 (May 2026):** Gap-closure release. Added 7 new nodes, hardened the executor with automatic Art. 5 + Art. 73 hooks, extended 6 existing primitives. +> +> **2026-09 conformance pass:** corrected the cybersecurity citation to **Art. 15(5)** (15(4) is robustness/feedback-loops in the enacted text); added **`Article27FRIANode`** for the deployer Art. 27 FRIA; added the 2 Dec 2026 Omnibus **NCII/CSAM** Art. 5 additions to `ProhibitedPracticeGuard`; the backbone jury now defaults to `always_human` with a real out-of-band decision path (no auto-approve); split `SyntheticMarkerNode` citations into Art. 50(2) (machine-readable) vs 50(1)/(4) (visible); made incident deadlines a conservative ceiling carrying all three Art. 73 legal deadlines. See the [Digital Omnibus section](#digital-omnibus-regulation-eu-20261744) below. --- @@ -100,7 +104,7 @@ The paper notes: *"agents accumulate interaction data... that may contain protec | Modality | LΓ‘r Primitive | Paper Reference | |---|---|---| -| **Retrospective** | `AuditLogger` + HMAC-SHA256 causal trace | Art. 12, prEN ISO/IEC 24970 | +| **Retrospective** | `AuditLogger` + HMAC-SHA256 causal trace | Art. 12, prEN ISO/IEC DIS 24970 | | **Real-time** | `TransparencyEngine` third-party disclosure | Art. 13, Art. 50 | | **Pre-execution** | `HumanJuryNode` blocking interrupt | Art. 14(4) | | **Authority record** | `AuthorityLedger` (Fourth Tier) | Paper fn. 18 | @@ -121,7 +125,7 @@ node_jury = HumanJuryNode( --- -### Step 7 β€” AI-Specific Cybersecurity (prEN 18282 / Art. 15(4)) +### Step 7 β€” AI-Specific Cybersecurity (prEN 18282 / Art. 15(5)) **Paper requirement (Β§6.1):** *"The inability to perform a restricted action [must] be enforced at the API level, where the model's tool interface simply does not expose the restricted capability."* Just-in-time credential provisioning, per-action authorization scoping, audit trails distinguishing user-initiated from AI-initiated actions. @@ -272,13 +276,38 @@ If all three legs are active and no human approval is on record, the guard raise ## v2.2.0 Gap-Closure Requirements -The following 12 requirements were identified as gaps in v2.1.x and are fully closed in v2.2.0. +The following requirements were identified as gaps in v2.1.x and addressed in v2.2.0 and the 2026-09 conformance pass. + +### A β€” Art. 9(2)(a): Runtime Fundamental-Rights Screening (NOT the Art. 27 FRIA) + +**Gap:** Art. 9(2)(a) requires the risk management system to identify and analyse the risks a high-risk system "can pose to health, safety or fundamental rights". v2.1.x had static design-time documentation but no runtime gate. + +**LΓ‘r:** `FundamentalRightsImpactNode` β€” a runtime heuristic that scans AI outputs against six EU Charter dimensions (Dignity Art. 1, Privacy Art. 7/8, Non-discrimination Art. 21, Expression Art. 11, Justice Art. 47, Data Protection Art. 8). Raises `FRIAViolation` and blocks or logs per configuration. -### A β€” Art. 9 FRIA: Fundamental Rights Impact Assessment +> **This is not the Article 27 FRIA.** The Act's named, standalone *Fundamental Rights Impact Assessment* is **Article 27** β€” a **deployer** obligation for (i) public bodies, (ii) private providers of public services, and (iii) deployers doing credit scoring (Annex III 5(b)) or life & health insurance risk pricing (Annex III 5(c)). It is implemented separately by **`Article27FRIANode`** (row M). -**Gap:** Art. 9 requires providers to assess impacts on fundamental rights as part of the risk management system. v2.1.x had static design-time documentation but no runtime gate. +### M β€” Art. 27: Deployer Fundamental Rights Impact Assessment -**LΓ‘r v2.2.0:** `FundamentalRightsImpactNode` β€” scans AI outputs at runtime against six EU Charter dimensions (Dignity Art. 1, Privacy Art. 7/8, Non-discrimination Art. 21, Expression Art. 11, Justice Art. 47, Data Protection Art. 8). Raises `FRIAViolation` and blocks or logs per configuration. +**Gap:** no artifact for the actual Art. 27 FRIA. + +**LΓ‘r:** `Article27FRIANode` β€” scope-gated on `deployer_class`; for an in-scope deployer it generates the Art. 27(1)(a)-(f) template (deployer processes; period/frequency; affected persons; specific fundamental-rights risks informed by Art. 13 provider info; human-oversight measures; measures on materialisation incl. governance + complaint mechanisms), flags the Art. 27(3) authority-notification duty, and allows an Art. 27(4) DPIA cross-reference (as reinforced by the Digital Omnibus). `as_markdown()` produces the filing-ready document; the backbone writes `enterprise_audit/fria_art27.md`. It also reports completeness and can run `strict=True` to block on missing elements. Using the node is not the assessment β€” the deployer supplies the substantive content and files it with the market surveillance authority. + +```python +from lar.compliance import Article27FRIANode + +fria27 = Article27FRIANode( + system_name="Credit Decision Agent", + deployer_class="CREDIT_SCORING", # Annex III 5(b) + annex_iii_point="5(b)", + deployment_process="...", # 27(1)(a) + usage_period_and_frequency="...", # 27(1)(b) + affected_natural_persons=["applicants", "guarantors"], # 27(1)(c) + fundamental_rights_risks=["proxy bias (Charter Art. 21)"], # 27(1)(d) + human_oversight_measures=["mandatory reviewer approval gate"], # 27(1)(e) + measures_on_materialisation=["suspend + notify authority", "complaint channel"], # 27(1)(f) + dpia_reference="DPIA-2026-014", # 27(4) +) +``` ```python from lar.compliance import FundamentalRightsImpactNode @@ -441,7 +470,7 @@ boundary = MultiAgentBoundaryNode( ### J β€” Art. 73–74: Real-Time Incident Detection -**Gap:** `IncidentReporter` (v2.1.x) was a post-hoc PMM report generator scanning log files. Art. 73 requires *real-time* incident detection with 24h reporting deadlines for serious incidents. No runtime hook existed. +**Gap:** `IncidentReporter` (v2.1.x) was a post-hoc PMM report generator scanning log files. Art. 73 requires prompt serious-incident reporting. No runtime hook existed. (The real deadlines are keyed to incident *type*, not a severity score: Art. 73(2) general default **15 days**; Art. 73(3) widespread infringement / serious incident per Art. 3(49)(b) **2 days**; Art. 73(4) death of a person **10 days**.) **LΓ‘r v2.2.0:** `IncidentReporterNode` β€” operates in two modes: 1. **Graph node mode** β€” scans state for harm signals (`last_error`, `_prohibited_practice_flag`, `fria_findings`, etc.) after each step. @@ -458,7 +487,7 @@ incident_reporter = IncidentReporterNode( executor = GraphExecutor(incident_reporter=incident_reporter) ``` -Incident records include the Art. 73 reporting deadline (`24h` for CRITICAL/HIGH, `72h` for MEDIUM) computed at detection time. +Because LΓ‘r cannot infer from a generic harm signal whether an incident is legally "widespread" or involved a death, each record carries a **conservative ceiling** (`48h` for CRITICAL/HIGH, `360h`/15 days for MEDIUM) *plus* the full `art_73_legal_deadlines` map and `provider_must_confirm_applicable_paragraph: true`. LΓ‘r deliberately does not assign the Art. 73(4) "death" deadline off a severity tier. --- @@ -484,7 +513,7 @@ mem_erase = SessionMemoryNode(mode="erase", subject_key="applicant_id") --- -### L β€” Art. 15(4): Trust-Based Privilege Restriction +### L β€” Art. 15(5): Trust-Based Privilege Restriction **Gap:** `CredentialVault.get()` provisioned credentials JIT but had no mechanism to restrict sensitive credentials to high-trust execution paths. A compromise path could obtain a payment API key before a HumanJuryNode had approved. @@ -510,11 +539,12 @@ cd lar/ python examples/compliance/22_eu_ai_act_finance_showcase.py ``` -The showcase (v2.2.0) runs the FINANCE backbone against a credit application with `ollama/phi4:latest`, validates all 23 Nannini et al. paper-mapped requirements, and produces three audit artifacts in `enterprise_audit/`: +The showcase runs the FINANCE backbone against a credit application (`ollama/phi4:latest` by default; override with `LAR_SHOWCASE_MODEL`), verifies **26 runtime rows**, and produces four artifacts in `enterprise_audit/`: -- `compliance_manifest.json` β€” Step 9 action inventory -- `authority_ledger.json` β€” Art. 14 oversight records +- `compliance_manifest.json` β€” Step 9 action inventory (Annex IV Β§2(b)) +- `authority_ledger.json` β€” Art. 12/14 oversight records (real out-of-band decision, not auto-approve) - `run_.json` β€” Art. 12 causal trace (HMAC signed, PII stripped) +- `fria_art27.md` β€” the deployer's Art. 27 Fundamental Rights Impact Assessment To run other domains: @@ -529,10 +559,26 @@ result = build_and_run(case=my_case, domain="LEGAL") --- +## Digital Omnibus (Regulation (EU) 2026/1744) + +The "Digital Omnibus on AI" was published 24 July 2026 and entered into force 27 July 2026. It amends the AI Act. What changed, and how LΓ‘r reflects it: + +| Change | Effect on LΓ‘r | +|---|---| +| **Annex III high-risk obligations deferred** β€” from 2 Aug 2026 to **2 December 2027** (Annex I embedded high-risk to 2 Aug 2028). | Docs/badges no longer say "ready Aug 2026". The Art. 8-15 / 16 / 26 / 27 machinery is what applies from Dec 2027. | +| **Art. 5 additions** β€” bans on non-consensual intimate imagery (NCII) and AI-generated CSAM, applicable **2 December 2026**. | `ProhibitedPracticeGuard` gains `NCII` and `CSAM` heuristic categories (on by default; `include_omnibus_categories=False` to disable). | +| **Art. 27(4) DPIA cross-reference** strengthened β€” a FRIA may incorporate/cross-refer a GDPR Art. 35 DPIA. | `Article27FRIANode(dpia_reference=...)` records the cross-reference. | +| **Annex VIII registration simplified**; **SME / small-mid-cap relief** β€” lighter technical documentation, proportionate QMS, cybersecurity presumption of conformity, priority sandbox access, reduced fine caps (< 750 employees and < €150m turnover). | Not code β€” but providers using LΓ‘r who qualify should apply the lighter Annex IV / QMS route. | +| **Art. 50 unchanged** β€” transparency applies from 2 Aug 2026; providers of generative systems already on the market have until **2 December 2026** for the 50(2) machine-readable marking. | `SyntheticMarkerNode` docstring carries the 2 Dec 2026 note; METADATA path = 50(2), VISIBLE path = 50(1)/(4). | + +Primary sources: [EUR-Lex β€” Regulation (EU) 2026/1744](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=OJ%3AL_202601744); [EUR-Lex β€” Regulation (EU) 2024/1689](https://eur-lex.europa.eu/eli/reg/2024/1689/oj); [Article 15](https://artificialintelligenceact.eu/article/15/), [Article 27](https://artificialintelligenceact.eu/article/27/), [Article 50](https://artificialintelligenceact.eu/article/50/); [CEN-CENELEC JTC 21 work programme](https://adamleonsmith.substack.com/p/the-cen-cenelec-jtc-21-work-programme). + +--- + ## References - Nannini, L., Smith, A.L., Maggini, M.J., Panai, E., Feliciano, S., Tiulkanov, A., Maran, E., Gealy, J., Bisconti, P. (2026). *AI Agents Under EU Law: A Compliance Architecture for AI Providers.* arXiv:2604.04604v1. -- EU AI Act (Regulation 2024/1689). Official Journal of the European Union. +- EU AI Act (Regulation (EU) 2024/1689) as amended by the Digital Omnibus (Regulation (EU) 2026/1744). Official Journal of the European Union. - AEPD (2026). *Guidance on GDPR obligations for agentic AI deployments.* February 2026. - Kim et al. (2025). *Systematic survey of the agentic AI attack and defense landscape.* - OWASP Agentic Security Initiative (2025). *Top 10 for Agentic Applications.* diff --git a/docs/core-concepts/4-core-patterns.md b/docs/core-concepts/4-core-patterns.md index 4565f75..6e931d2 100644 --- a/docs/core-concepts/4-core-patterns.md +++ b/docs/core-concepts/4-core-patterns.md @@ -61,7 +61,7 @@ See `examples/compliance/5_context_contamination_test.py` and `6_zombie_action_t | **13** | **[`12_transparency_disclosure.py`](../../examples/compliance/12_transparency_disclosure.py)** | Transparency Engine (Art. 13) | | **14** | **[`13_risk_scored_routing.py`](../../examples/compliance/13_risk_scored_routing.py)** | Risk-Scored Routing (Art. 14) | | **15** | **[`14_runtime_drift_detection.py`](../../examples/compliance/14_runtime_drift_detection.py)** | Drift Detection (Art. 3(23)) | -| **16** | **[`15_jit_credential_vault.py`](../../examples/compliance/15_jit_credential_vault.py)** | JIT Credential Vault (Art. 15(4)) | +| **16** | **[`15_jit_credential_vault.py`](../../examples/compliance/15_jit_credential_vault.py)** | JIT Credential Vault (Art. 15(5)) | | **17** | **[`16_pii_redaction.py`](../../examples/compliance/16_pii_redaction.py)** | PII Redaction (GDPR Art. 17) | | **18** | **[`17_causal_trace_logging.py`](../../examples/compliance/17_causal_trace_logging.py)** | Causal Trace Logging (Art. 12) | | **19** | **[`18_synthetic_content_marking.py`](../../examples/compliance/18_synthetic_content_marking.py)** | Synthetic Content Marking (Art. 50) | diff --git a/docs/guides/build-compliant-agent.md b/docs/guides/build-compliant-agent.md index c6444b0..2b85caa 100644 --- a/docs/guides/build-compliant-agent.md +++ b/docs/guides/build-compliant-agent.md @@ -16,7 +16,7 @@ The EU AI Act (enforced August 2026) requires that high-risk AI systems: - Keep records of every decision (Art. 12) - Allow humans to override before irreversible actions (Art. 14) - Manage risk continuously (Art. 9) -- Protect against cybersecurity threats including credential abuse (Art. 15(4)) +- Protect against cybersecurity threats including credential abuse (Art. 15(5)) LΓ‘r satisfies these requirements at the execution layer β€” not through documentation written after the fact, but through primitives that fire during the run. Each step in this guide introduces one primitive, explains which regulation it satisfies, and shows you exactly how to wire it in. @@ -72,9 +72,9 @@ case = { --- -## Step 2 β€” Credential Vault (Art. 15(4)) +## Step 2 β€” Credential Vault (Art. 15(5)) -**Regulation:** Art. 15(4) requires just-in-time privilege minimisation. The agent must not hold standing credentials. +**Regulation:** Art. 15(5) requires high-risk systems to be resilient against unauthorised third parties exploiting vulnerabilities. Just-in-time, scope-limited credentials β€” so the model never holds a standing key β€” is a proportionate security measure under 15(5) (aligned with prEN 18282 and the CRA's least-privilege principle). Not verbatim 15(5) text, but the measure the Article calls for. The `CredentialVault` stores credentials and issues scoped, single-use tokens at the moment of the specific action. The agent never holds the raw key. diff --git a/docs/index.md b/docs/index.md index ad3f14e..a2f7988 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,7 +23,7 @@ LΓ‘r ships a complete, production-ready **Enterprise Compliance Backbone** β€” 2 | `ComplianceManifest` (Annex IV auto-generation) | Art. 11 β€” Technical Documentation | | `SyntheticMarkerNode` | Art. 50(2) β€” Synthetic Content Marking | | `BiasFilter` | prEN 18283 β€” Bias Management | -| `CredentialVault` (JIT provisioning) | Art. 15(4) β€” Privilege Minimisation | +| `CredentialVault` (JIT provisioning) | Art. 15(5) β€” Privilege Minimisation | | `RuntimeStateVersioner` (drift detection) | Art. 3(23) β€” Substantial Modification | | `AuthorityLedger` β€” HMAC-signed decision record | Art. 12 / 14 β€” Fourth-Tier Oversight | | `BranchTriageNode` β€” per-branch findings before jury | Art. 14 β€” Fractal Human Oversight | diff --git a/examples/compliance/22_eu_ai_act_finance_showcase.py b/examples/compliance/22_eu_ai_act_finance_showcase.py index def6c2a..7699d64 100644 --- a/examples/compliance/22_eu_ai_act_finance_showcase.py +++ b/examples/compliance/22_eu_ai_act_finance_showcase.py @@ -1,118 +1,71 @@ """ EU AI Act Finance Agent Showcase ================================ -Demonstrates the LΓ‘r Enterprise Compliance Backbone running a high-risk -credit application through all 23 requirements from: - - Nannini et al. (2026) "AI Agents Under EU Law: A Compliance Architecture - for AI Providers" β€” April 7, 2026 (arXiv:2604.04604v1) - -Every requirement row marked "βœ… Runtime" actually fires during this run: - - Rows S0-S11: the original 12-step backbone nodes - - Rows A-L: the v2.2.0 gap-closure nodes β€” all wired into the live graph - -HOW LÁR ADDRESSES THE PAPER'S 12-STEP COMPLIANCE SEQUENCE ------------------------------------------------------------ -The paper (Section 8.1) defines a 12-step sequence. Here is how LΓ‘r maps to each: - -Step 0 β€” Scope (Art. 3(1)): - This agent invokes an LLM, processes personal data, and produces credit - recommendations affecting third parties. It satisfies every element of - Art. 3(1): varying autonomy, adaptiveness, outputs influencing environments. - Classification: HIGH-RISK under Annex III, point 5(b) (creditworthiness). - -Step 1 β€” GPAI Layer (Art. 53): - The backbone is model-agnostic via LiteLLM. When using third-party GPAI - models (GPT-4, Gemini), providers must obtain Art. 53 technical documentation - from the upstream supplier and integrate known model limitations into their - Art. 9 risk process. This is a provider-level legal obligation outside the - runtime framework. The backbone's domain config documents the model used. - -Step 2 β€” Classification: - Hard-coded as FINANCE / Annex III, point 5(b) (creditworthiness assessment). - The 'conformity_id' field in DOMAIN_PRESETS serves as the classification - record. Production systems must document classification reasoning per - Art. 6(2) with sufficient specificity to survive regulatory scrutiny. - -Step 3 β€” QMS (prEN 18286): - The Quality Management System is an organisational process, not a runtime - artifact. The backbone produces the technical documentation artifacts that - a QMS requires: compliance manifest (Annex IV inventory), authority ledger - (oversight records), causal trace (audit log). These feed the QMS's - post-market monitoring obligation (clause 9.4). - -Step 4 β€” Risk Management (prEN 18228 / Art. 9): - β†’ PolicyRegistry: registers every action with risk_tier, reversibility, - oversight_level, and affected_parties. - β†’ RiskScorerNode: scores each action pre-execution and routes to - HumanJuryNode when risk threshold is exceeded. - -Step 5 β€” Data Governance (prEN 18284 / prEN 18283 / Art. 10): - β†’ PIIRedactionEngine: strips sensitive keys (SSN, IBAN, name) from the - causal trace before HMAC signing β€” satisfies GDPR Art. 17 data minimisation. - β†’ BiasFilterNode: scans LLM output for protected-characteristic terms - (age, gender, race, nationality) per prEN 18283 bias management. - -Step 6 β€” Trustworthiness (prEN 18229-1 / Art. 12–14): - β†’ AuditLogger + HMAC-SHA256: immutable, cryptographically signed causal - trace satisfying Art. 12 logging requirements. - β†’ HumanJuryNode: blocking pre-execution interrupt satisfying Art. 14 - human oversight for irreversible high-risk actions. - β†’ AuthorityLedger: records stakeholder identity, role, decision, and - rationale β€” the "fourth tier" of oversight (paper Β§6.2, fn. 18). - β†’ TransparencyEngine: flags Art. 50 disclosure obligation to third parties - affected by the agent's external write action. - -Step 7 β€” AI-Specific Cybersecurity (prEN 18282 / Art. 15(4)): - β†’ CredentialVault: just-in-time NHI credential provisioning. The agent - receives credentials only for the specific action it is about to perform. - Satisfies the paper's Β§6.1 requirement for "JIT credential provisioning" - and "per-action authorization scoping" outside the generative model. - -Step 8 β€” CRA Applicability: - If deployed as standalone software with network connectivity (VS Code - extension, CLI tool, API service), full CRA conformity applies from - Dec 2027 with vulnerability reporting from Sep 2026. The backbone's - HMAC signing and CredentialVault architecture align with CRA Annex I - secure-by-design requirements. CRA assessment is provider-level β€” not - a runtime artifact. - -Step 9 β€” Adjacent Legislation Map (paper Β§8.1, Step 9): - β†’ ComplianceManifestGenerator: exhaustive inventory of all external - actions, affected parties, data flows, and regulatory triggers per - node. This is the paper's "foundational compliance task" β€” the - regulatory map that activates GDPR, MiFID II, DORA, and Art. 50 - based on what the agent actually does at runtime. - -Step 10 β€” Conformity Assessment: - β†’ The manifest provides the Annex IV technical documentation foundation. - β†’ The authority ledger provides the oversight records. - β†’ The causal trace provides the audit trail. - These artifacts are the inputs to a conformity assessment body review. - The provider prepares the EU Declaration of Conformity and registers - in the EU database (production obligation, not a runtime artifact). - -Step 11 β€” Post-Market Monitoring & Drift Detection (Art. 3(23)): - β†’ RuntimeStateVersioner: snapshots tool_catalogue, policy_bindings, and - state_schema_keys at conformity baseline. Detects runtime drift and - warns when the operational profile deviates from the assessed baseline. - If drift crosses the Art. 3(23) threshold, a new conformity assessment - is required. - -v2.2.0 Gap-Closure (Rows A–L) β€” All Fired at Runtime ------------------------------------------------------- -A β€” FundamentalRightsImpactNode: Runs after LLM; scans for EU Charter violations. -B β€” BehavioralEnvelopeMonitor: Observes confidence against PMM baseline in parse step. -C β€” AuditLogger.verify_step_integrity: Called post-run; recomputes each step's diff. -D β€” AuditLogger.log_plan_switch: Logged in compliance_checks when jury path taken. -E β€” DeployerTransparencyNode: First graph node; writes deployer_instructions. -F β€” HumanJuryNode(automation_boundary): automation_boundary="always_human" enforced. -G β€” SupplierAgreementRegistry: assert_agreement() called before llm_gateway + external_write. -H β€” DynamicToolDiscoveryMonitor: Graph node after jury; compares live vs. baseline catalogue. -I β€” MultiAgentBoundaryNode: Second graph node; declares INTERNAL boundary. -J β€” IncidentReporterNode: Auto-wired into GraphExecutor; fires on any exception. -K β€” SessionMemoryNode: write β†’ erase within single run (Art. 17 demonstrated). -L β€” CredentialVault.get_with_trust: Called with trust_level="HIGH"; raises if trust insufficient. +Runs a high-risk credit application (Annex III, point 5(b) β€” creditworthiness) +through the LΓ‘r Enterprise Compliance Backbone and verifies, at runtime, that +every wired primitive actually fires. + +What "covered" means here +------------------------- +A row is "covered" when a runtime hook fires and/or an evidence artifact is +produced. It does **not** mean the underlying legal obligation is discharged. +Conformity assessment (Art. 43), the quality management system (Art. 17), the +EU declaration of conformity (Art. 47), EU-database registration (Art. 49), and +the *substantive content* of every assessment remain the customer-provider's and +the deployer's responsibility. LΓ‘r is a software component in the Art. 25 value +chain, not the provider (see README "Who is the Provider?"). + +Legal basis +----------- +Regulation (EU) 2024/1689 (the AI Act) as amended by Regulation (EU) 2026/1744 +(the "Digital Omnibus on AI", in force 27 July 2026). Architecture mapped to +Nannini et al. (2026), "AI Agents Under EU Law" (arXiv:2604.04604v1) β€” a +secondary source; article citations below are verified against the Regulation. + +Application timeline (post-Omnibus) +----------------------------------- + * Art. 5 prohibited practices β€” since 2 Feb 2025; NCII / AI-CSAM additions + from 2 Dec 2026. + * GPAI provider obligations β€” since 2 Aug 2025. + * Art. 50 transparency β€” from 2 Aug 2026 (2 Dec 2026 grace for the 50(2) + machine-readable marking of generative systems already on the market). + * Annex III high-risk obligations (Art. 8-15, 16, 26, 27, ...) β€” from + **2 December 2027** (deferred from 2 Aug 2026 by the Omnibus). + * Annex I embedded high-risk AI β€” from 2 Aug 2028. + +Human oversight (Art. 14) in this showcase +------------------------------------------ +The HumanJuryNode is configured ``automation_boundary={"case_analysis": +"always_human"}``. With no reviewer and no TTY it HALTS (RuntimeError) β€” it never +auto-approves. This showcase supplies a **simulated** reviewer through +``human_decision_provider`` (built from ``_mock_inputs``); that decision travels +the same validated + AuthorityLedger-recorded path as a real one. A production +deployment wires ``human_decision_provider`` to a real reviewer UI / web form / +Slack action, or runs interactively. + +Row map (all fire at runtime unless marked Docs/Artifacts) +--------------------------------------------------------- + S4 Art. 9 PolicyRegistry + RiskScorerNode + S5 Art. 10/17 PIIRedactionEngine (GDPR Art. 17) + BiasFilterNode (10(2)(f)-(g)) + S6 Art. 12-14 AuditLogger(HMAC) + HumanJuryNode + AuthorityLedger + TransparencyEngine + S7 Art. 15(5) CredentialVault (cybersecurity β€” NHI least privilege, JIT + trust) + S9 Step 9 ComplianceManifestGenerator (+ adjacent-legislation map) + S11 Art. 3(23) RuntimeStateVersioner + BehavioralEnvelopeMonitor + A Art. 9(2)(a) FundamentalRightsImpactNode (runtime screen β€” NOT the Art. 27 FRIA) + B Art. 9(9) BehavioralEnvelopeMonitor + C Art. 12 AuditLogger.verify_step_integrity() + D Art. 12 AuditLogger.log_plan_switch() + E Art. 13 DeployerTransparencyNode (instructions for use) + F Art. 14 HumanJuryNode(automation_boundary="always_human") + real decision path + G Art. 25(4) SupplierAgreementRegistry (FOSS carve-out noted) + H Art. 3(23) DynamicToolDiscoveryMonitor + I Art. 3 MultiAgentBoundaryNode + J Art. 73 IncidentReporterNode (48h/360h conservative ceiling + legal deadlines) + K GDPR Art. 17 SessionMemoryNode (write + erase) + L Art. 15(5) CredentialVault.get_with_trust() + M Art. 27 Article27FRIANode β€” deployer Fundamental Rights Impact Assessment + N Art. 5 ProhibitedPracticeGuard incl. 2 Dec 2026 NCII/CSAM additions + O Art. 50(2) SyntheticMarkerNode METADATA (machine-readable) + 50(1)/(4) VISIBLE Usage: python examples/compliance/22_eu_ai_act_finance_showcase.py @@ -129,94 +82,105 @@ def print_paper_coverage_table(): - """Print a table showing LΓ‘r v2.2.0 coverage of all 23 paper-mapped requirements.""" - table = Table(title="Nannini et al. (2026) β€” Full Coverage Map (v2.2.0)", show_lines=True) - table.add_column("Ref", style="bold cyan", width=6) - table.add_column("Paper Requirement", width=34) - table.add_column("LΓ‘r v2.2.0 Primitive", width=32) - table.add_column("Status", width=12) + table = Table(title="EU AI Act coverage β€” LΓ‘r Enterprise Backbone", show_lines=True) + table.add_column("Ref", style="bold cyan", width=5) + table.add_column("Provision", width=16) + table.add_column("Primitive", width=34) + table.add_column("Kind", width=11) rows = [ - # ── Original 12 steps ──────────────────────────────────────────────── - ("S0", "Scope: Art. 3(1) AI system definition", "Domain config + classification doc", "βœ… Docs"), - ("S1", "GPAI layer: Art. 53 documentation chain", "Model-agnostic (LiteLLM) + config", "βœ… Docs"), - ("S2", "Classify: Annex III / high-risk determination", "DOMAIN_PRESETS + conformity_id", "βœ… Docs"), - ("S3", "QMS: prEN 18286 lifecycle management", "Manifest + Ledger + Causal Trace", "βœ… Artifacts"), - ("S4", "Risk Mgmt: prEN 18228 / Art. 9", "PolicyRegistry + RiskScorerNode", "βœ… Runtime"), - ("S5", "Data Gov: prEN 18284 / prEN 18283 / Art. 10", "PIIRedactionEngine + BiasFilterNode", "βœ… Runtime"), - ("S6", "Trustworthiness: Art. 12–14", "AuditLogger + HumanJuryNode + Ledger", "βœ… Runtime"), - ("S7", "Cybersecurity: prEN 18282 / Art. 15(4)", "CredentialVault (JIT + trust-based)", "βœ… Runtime"), - ("S8", "CRA applicability", "Secure-by-design architecture", "βœ… Docs"), - ("S9", "Adjacent legislation inventory", "ComplianceManifestGenerator + DOMAIN_MAP", "βœ… Runtime"), - ("S10", "Conformity assessment artifacts", "Manifest + Ledger + Trace β†’ Annex IV", "βœ… Artifacts"), - ("S11", "Post-market monitoring + drift", "RuntimeStateVersioner + BehavioralEnvMon", "βœ… Runtime"), - # ── v2.2.0 gap-closure rows ─────────────────────────────────────────── - ("A", "Art. 9 FRIA β€” Fundamental Rights Impact", "FundamentalRightsImpactNode", "βœ… Runtime"), - ("B", "Art. 9 PMM β€” Output variance monitoring", "BehavioralEnvelopeMonitor", "βœ… Runtime"), - ("C", "Art. 12 causal chain β€” per-step integrity", "AuditLogger.verify_step_integrity()", "βœ… Runtime"), - ("D", "Art. 12 causal chain β€” plan-switch events", "AuditLogger.log_plan_switch()", "βœ… Runtime"), - ("E", "Art. 13 deployer instructions-for-use", "DeployerTransparencyNode", "βœ… Runtime"), - ("F", "Art. 14 automation boundary per decision type", "HumanJuryNode(automation_boundary=...)", "βœ… Runtime"), - ("G", "Art. 25(4) written supplier agreements", "SupplierAgreementRegistry", "βœ… Runtime"), - ("H", "Art. 3(23) post-conformity tool addition", "DynamicToolDiscoveryMonitor", "βœ… Runtime"), - ("I", "Art. 3 sub-agent boundary classification", "MultiAgentBoundaryNode", "βœ… Runtime"), - ("J", "Art. 73-74 real-time incident reporting", "IncidentReporterNode (executor hook)", "βœ… Runtime"), - ("K", "GDPR Art. 17 erasable per-subject memory", "SessionMemoryNode (write/erase)", "βœ… Runtime"), - ("L", "Art. 15(4) trust-based privilege restriction", "CredentialVault.get_with_trust()", "βœ… Runtime"), + ("S0", "Art. 3(1)", "Domain config + classification doc", "Docs"), + ("S1", "Art. 53 (GPAI)", "LiteLLM model-agnostic + config", "Docs"), + ("S2", "Annex III / Art.6","DOMAIN_PRESETS + conformity_id", "Docs"), + ("S3", "Art. 17 (QMS)", "Manifest + Ledger + Causal Trace", "Artifacts"), + ("S4", "Art. 9", "PolicyRegistry + RiskScorerNode", "Runtime"), + ("S5", "Art. 10 / GDPR 17","PIIRedactionEngine + BiasFilterNode", "Runtime"), + ("S6", "Art. 12-14", "AuditLogger + HumanJuryNode + Ledger", "Runtime"), + ("S7", "Art. 15(5)", "CredentialVault (JIT + trust) / prEN 18282", "Runtime"), + ("S8", "CRA", "Secure-by-design architecture", "Docs"), + ("S9", "Step 9", "ComplianceManifestGenerator + DOMAIN_MAP", "Runtime"), + ("S10", "Art. 11 / Annex IV","Manifest Β§2 + Ledger + Trace", "Artifacts"), + ("S11", "Art. 3(23)", "RuntimeStateVersioner + BehavioralEnvMon", "Runtime"), + ("A", "Art. 9(2)(a)", "FundamentalRightsImpactNode (screen)", "Runtime"), + ("B", "Art. 9(9)", "BehavioralEnvelopeMonitor", "Runtime"), + ("C", "Art. 12", "AuditLogger.verify_step_integrity()", "Runtime"), + ("D", "Art. 12", "AuditLogger.log_plan_switch()", "Runtime"), + ("E", "Art. 13", "DeployerTransparencyNode", "Runtime"), + ("F", "Art. 14", "HumanJuryNode(always_human) + real path", "Runtime"), + ("G", "Art. 25(4)", "SupplierAgreementRegistry", "Runtime"), + ("H", "Art. 3(23)", "DynamicToolDiscoveryMonitor", "Runtime"), + ("I", "Art. 3", "MultiAgentBoundaryNode", "Runtime"), + ("J", "Art. 73", "IncidentReporterNode (executor hook)", "Runtime"), + ("K", "GDPR Art. 17", "SessionMemoryNode (write/erase)", "Runtime"), + ("L", "Art. 15(5)", "CredentialVault.get_with_trust()", "Runtime"), + ("M", "Art. 27 (FRIA)", "Article27FRIANode (deployer FRIA)", "Runtime"), + ("N", "Art. 5 (+Omnibus)","ProhibitedPracticeGuard (NCII/CSAM)", "Runtime"), + ("O", "Art. 50(1)/(2)/(4)","SyntheticMarkerNode VISIBLE + METADATA", "Runtime"), ] - for ref, req, primitive, status in rows: - table.add_row(ref, req, primitive, status) + for ref, prov, primitive, kind in rows: + table.add_row(ref, prov, primitive, kind) console.print(table) - runtime_count = sum(1 for *_, s in rows if "Runtime" in s) - artifact_count = sum(1 for *_, s in rows if "Artifact" in s) - docs_count = sum(1 for *_, s in rows if "Docs" in s) + r = sum(1 for *_, k in rows if k == "Runtime") + a = sum(1 for *_, k in rows if k == "Artifacts") + d = sum(1 for *_, k in rows if k == "Docs") console.print( - f"[dim]Total: {len(rows)} requirements mapped β€” " - f"{runtime_count} Runtime Β· {artifact_count} Artifacts Β· {docs_count} Docs[/dim]" + f"[dim]{len(rows)} rows β€” {r} Runtime Β· {a} Artifacts Β· {d} Docs. " + f"'Covered' = runtime hook + evidence, NOT full obligation discharge.[/dim]" ) def _check(label: str, condition: bool, detail: str = "") -> bool: - """Print a pass/fail line and return the condition.""" - mark = "[green]βœ“[/green]" if condition else "[red]βœ—[/red]" + mark = "[green]OK[/green]" if condition else "[red]FAIL[/red]" console.print(f" {mark} {label}" + (f" [dim]{detail}[/dim]" if detail else "")) - return condition + return bool(condition) def main(): console.print(Panel.fit( "[bold green] EU AI Act Finance Agent Showcase [/bold green]\n" - "[dim]Nannini et al. (2026) β€” Full Coverage Architecture v2.2.0[/dim]", - subtitle="23 Requirements Mapped Β· All Exercised at Runtime" + "[dim]Regulation (EU) 2024/1689 as amended by Regulation (EU) 2026/1744 " + "(Digital Omnibus)[/dim]", + subtitle="Runtime hook + evidence per provision β€” not a conformity assessment" )) - console.print("\n[bold cyan]Paper Coverage Map[/bold cyan]") + console.print(Panel.fit( + "[yellow]Timeline:[/yellow] Annex III high-risk obligations apply from " + "[bold]2 Dec 2027[/bold] (deferred by the Omnibus). Art. 5 (since Feb 2025; " + "NCII/CSAM from 2 Dec 2026), GPAI (since Aug 2025) and Art. 50 (since Aug 2026, " + "2 Dec 2026 grace for 50(2) marking) are unaffected.", + title="Application dates" + )) + + console.print("\n[bold cyan]Coverage map[/bold cyan]") print_paper_coverage_table() - # High-risk credit application with PII (will be stripped before audit log signing) case_data = { "name": "Jane Doe", - "ssn": "000-00-0000", + "ssn": "000-00-0000", "account_number": "ACT-99281-XYZ", "email": "jane.doe@example.com", "dob": "1985-04-12", - "applicant_id": "APP-2026-00923", # used by SessionMemoryNode subject key + "applicant_id": "APP-2026-00923", "case_summary": ( - "Credit application for a €500,000 SME loan. " + "Credit application for a EUR 500,000 SME loan. " "Applicant is a 39-year-old female business owner. " "Current debt-to-equity ratio is 4.2. " "Three missed payments on existing credit lines in the last 18 months." - ) + ), } console.print("\n[bold cyan]Step 1: Intake & Setup[/bold cyan]") - console.print("Processing a high-risk credit application (Annex III, point 5(b)) containing PII.") - console.print("[dim]Classification: FINANCE / HIGH-RISK / PRE_EXECUTION oversight / THIRD_PARTY affected[/dim]") + console.print("High-risk credit application (Annex III, point 5(b)) containing PII.") + console.print("[dim]FINANCE / HIGH-RISK / PRE_EXECUTION oversight / THIRD_PARTY affected / " + "deployer_class=CREDIT_SCORING -> Art. 27 FRIA applies[/dim]") + # DEMO reviewer: [decision, rationale]. build_and_run turns this into a + # human_decision_provider so the always_human jury runs without a TTY. mock_human_inputs = [ "approve", - "Reviewed FINANCE case. AI recommendation verified against policy. Approving with mandatory collateral condition.", + "Reviewed FINANCE case. AI recommendation checked against credit policy; " + "approving subject to mandatory collateral and a fair-lending second check.", ] try: @@ -230,167 +194,171 @@ def main(): raise console.print("\n[bold cyan]Step 2: Pipeline Complete[/bold cyan]") - console.print(f"System: [bold yellow]{result['system_name']}[/bold yellow]") - console.print(f"Domain: [bold blue]{result['domain']}[/bold blue]") + console.print(f"System: [bold yellow]{result['system_name']}[/bold yellow] " + f"Domain: [bold blue]{result['domain']}[/bold blue] " + f"Demo reviewer: [bold]{result.get('demo_human_decision')}[/bold]") - # ── Verification: check every requirement actually fired ───────────────── - console.print("\n[bold cyan]Step 3: Runtime Verification (all 23 requirements)[/bold cyan]") - - fs = result.get("final_state", {}) # final graph state + fs = result.get("final_state", {}) passes = [] - # ── Original 12 steps ───────────────────────────────────────────────────── - console.print("\n[bold]Original 12-Step Requirements[/bold]") + console.print("\n[bold]Core requirements[/bold]") - # S4: PolicyRegistry + RiskScorerNode - passes.append(_check("S4 β€” PolicyRegistry + RiskScorerNode", - fs.get("computed_oversight_level") is not None - or fs.get("jury_decision") is not None, + passes.append(_check("S4 - PolicyRegistry + RiskScorerNode (Art. 9/14)", + fs.get("computed_oversight_level") is not None or fs.get("jury_decision") is not None, f"jury_decision={fs.get('jury_decision')}")) - # S5: PII redaction + BiasFilterNode audit_path = result["audit_log_path"] with open(audit_path) as f: audit_text = f.read() pii_clean = ("Jane Doe" not in audit_text and "000-00-0000" not in audit_text) - passes.append(_check("S5a β€” PII redacted from causal trace (GDPR Art. 17)", pii_clean)) - passes.append(_check("S5b β€” BiasFilterNode executed", - fs.get("bias_detected") is not None, - f"bias_detected={fs.get('bias_detected')}")) + passes.append(_check("S5a - PII redacted from causal trace (GDPR Art. 17)", pii_clean)) + passes.append(_check("S5b - BiasFilterNode ran (Art. 10(2)(f)-(g) keyword gate)", + fs.get("bias_detected") is not None, f"bias_detected={fs.get('bias_detected')}")) - # S6: HMAC trace + HumanJuryNode + AuthorityLedger hmac_present = ("Signature:" in audit_text or '"hmac"' in audit_text or '"signature"' in audit_text.lower()) - passes.append(_check("S6a β€” HMAC-SHA256 signature on causal trace (Art. 12)", hmac_present)) - passes.append(_check("S6b β€” HumanJuryNode decision recorded (Art. 14)", + passes.append(_check("S6a - HMAC-SHA256 signature on causal trace (Art. 12)", hmac_present)) + passes.append(_check("S6b - HumanJuryNode decision recorded (Art. 14)", fs.get("jury_decision") in ("approve", "reject"))) + ledger_path = result["authority_ledger_path"] with open(ledger_path) as f: ledger_data = json.load(f) - has_records = isinstance(ledger_data, dict) and bool(ledger_data.get("records")) - passes.append(_check("S6c β€” AuthorityLedger Fourth-Tier record (Art. 12/14)", has_records)) - if has_records: - rec = ledger_data["records"][-1] + recs = ledger_data.get("records", []) if isinstance(ledger_data, dict) else [] + rec = recs[-1] if recs else {} + passes.append(_check("S6c - AuthorityLedger Fourth-Tier record present (Art. 12/14)", + bool(recs) and bool(rec.get("rationale")))) + if rec: console.print(f" Stakeholder: {rec.get('stakeholder_id')} " - f"({rec.get('stakeholder_role')}) β€” {rec.get('decision')}") + f"({rec.get('stakeholder_role')}) -> {rec.get('decision')} " + f"rationale: {str(rec.get('rationale'))[:60]}...") - # S7: CredentialVault JIT token - passes.append(_check("S7 β€” CredentialVault JIT token provisioned (Art. 15(4))", + passes.append(_check("S7 - CredentialVault JIT token provisioned (Art. 15(5))", fs.get("jit_token_present") is True)) - # S9: ComplianceManifestGenerator manifest_path = result["manifest_path"] with open(manifest_path) as f: manifest_data = json.load(f) summary = manifest_data.get("summary", {}) - passes.append(_check("S9 β€” Action inventory generated (Step 9)", - summary.get("total_external_actions", 0) > 0, + manifest_15_5 = "15(5)" in json.dumps(manifest_data) and "15(4)" not in json.dumps( + {k: v for k, v in manifest_data.items() if k != "action_inventory"}) + passes.append(_check("S9 - Action inventory generated (Step 9); cites Art. 15(5)", + summary.get("total_external_actions", 0) > 0 and manifest_15_5, f"external={summary.get('total_external_actions')}, " f"third_party={summary.get('third_party_affecting_actions')}")) - # S11: RuntimeStateVersioner drift check - passes.append(_check("S11 β€” Post-market drift snapshot (Art. 3(23))", + passes.append(_check("S11 - Post-market drift snapshot (Art. 3(23))", fs.get("drift_report") is not None)) - # ── v2.2.0 Gap-Closure: Rows A–L ───────────────────────────────────────── - console.print("\n[bold]v2.2.0 Gap-Closure Rows A–L[/bold]") + console.print("\n[bold]Gap-closure rows A-O[/bold]") - # Row A: FundamentalRightsImpactNode - passes.append(_check("A β€” FRIA executed; fria_passed written (Art. 9 FRIA)", + passes.append(_check("A - Fundamental-rights screen ran (Art. 9(2)(a) - NOT the Art. 27 FRIA)", fs.get("fria_passed") is not None, - f"fria_passed={fs.get('fria_passed')}, " - f"findings={len(fs.get('fria_findings') or [])}")) + f"fria_passed={fs.get('fria_passed')}, findings={len(fs.get('fria_findings') or [])}")) - # Row B: BehavioralEnvelopeMonitor envelope = fs.get("envelope_report") or {} - passes.append(_check("B β€” BehavioralEnvelopeMonitor observed (Art. 9 PMM)", + passes.append(_check("B - BehavioralEnvelopeMonitor observed (Art. 9(9) PMM)", "relative_deviation" in envelope or "deviation_exceeded" in envelope, - f"deviation_exceeded={envelope.get('deviation_exceeded')}, " - f"relative_deviation={envelope.get('relative_deviation', 'N/A')}")) + f"deviation_exceeded={envelope.get('deviation_exceeded')}")) - # Row C: verify_step_integrity integrity = result.get("integrity_results", []) - all_ok = all(r.get("integrity") == "OK" for r in integrity) if integrity else False - passes.append(_check("C β€” verify_step_integrity β€” all steps OK (Art. 12)", - all_ok, - f"{len(integrity)} steps verified, " - f"{sum(1 for r in integrity if r.get('integrity') == 'OK')} passed")) - - # Row D: log_plan_switch - plan_switches = [e for e in audit_text.split("\n") if "PLAN_SWITCH" in e] - passes.append(_check("D β€” log_plan_switch recorded in causal trace (Art. 12)", - len(plan_switches) > 0 or "plan_switch" in audit_text.lower(), - f"PLAN_SWITCH events in log: {len(plan_switches)}")) - - # Row E: DeployerTransparencyNode + all_ok = bool(integrity) and all(r.get("integrity") == "OK" for r in integrity) + passes.append(_check("C - verify_step_integrity - all steps OK (Art. 12)", + all_ok, f"{sum(1 for r in integrity if r.get('integrity') == 'OK')}/{len(integrity)} OK")) + + passes.append(_check("D - log_plan_switch recorded in causal trace (Art. 12)", + "plan_switch" in audit_text.lower() or "PLAN_SWITCH" in audit_text)) + deployer_ok = isinstance(fs.get("deployer_instructions"), dict) and bool( - fs.get("deployer_instructions", {}).get("system_name") + fs.get("deployer_instructions", {}).get("system_name")) + passes.append(_check("E - DeployerTransparencyNode wrote instructions-for-use (Art. 13)", + deployer_ok)) + + # F: always_human policy + a real (simulated) decision, with a signed rationale. + f_ok = ( + fs.get("jury_decision") in ("approve", "reject") + and bool(rec.get("rationale")) + and result.get("demo_human_decision") is True ) - passes.append(_check("E β€” DeployerTransparencyNode wrote deployer_instructions (Art. 13)", - deployer_ok, - f"system_name={fs.get('deployer_instructions', {}).get('system_name', '?')[:40]}")) - - # Row F: HumanJuryNode automation_boundary (auto_first_choice in CI; always_human in prod) - passes.append(_check("F β€” HumanJuryNode automation_boundary configured (Art. 14)", - fs.get("jury_decision") in ("approve", "reject"), - f"jury_decision={fs.get('jury_decision')} " - f"(automation_boundary wired; use 'always_human' in production)")) - - # Row G: SupplierAgreementRegistry - passes.append(_check("G β€” SupplierAgreementRegistry assert_agreement passed (Art. 25(4))", + passes.append(_check("F - HumanJuryNode always_human + recorded human decision (Art. 14)", + f_ok, + "policy=always_human; decision via human_decision_provider " + "(HALTS with no provider + no TTY)")) + + passes.append(_check("G - SupplierAgreementRegistry assert_agreement passed (Art. 25(4))", fs.get("supplier_agreements_verified") is True)) - # Row H: DynamicToolDiscoveryMonitor discovery = fs.get("tool_discovery_report") or {} - passes.append(_check("H β€” DynamicToolDiscoveryMonitor executed (Art. 3(23))", - "new_tools" in discovery and "baseline_count" in discovery, - f"new_tools={discovery.get('new_tools', [])}, " - f"baseline_count={discovery.get('baseline_count', 'N/A')}, " - f"substantial_mod={discovery.get('substantial_modification_flag')}")) + passes.append(_check("H - DynamicToolDiscoveryMonitor ran (Art. 3(23))", + "new_tools" in discovery, + f"new_tools={discovery.get('new_tools', [])}")) - # Row I: MultiAgentBoundaryNode boundaries = fs.get("multi_agent_boundaries") or [] - passes.append(_check("I β€” MultiAgentBoundaryNode boundary record written (Art. 3)", + passes.append(_check("I - MultiAgentBoundaryNode boundary record written (Art. 3)", len(boundaries) > 0, f"placement={boundaries[0].get('placement') if boundaries else 'N/A'}")) - # Row J: IncidentReporterNode (executor hook β€” fires on exceptions only) - passes.append(_check("J β€” IncidentReporterNode wired into executor (Art. 73-74)", - True, # Always wired; would have fired had any node raised - f"incident_log={result.get('incident_log_path')}")) + passes.append(_check("J - IncidentReporterNode wired into executor (Art. 73)", + result.get("incident_log_path") is not None, + "conservative 48h/360h ceiling + all three Art. 73 deadlines per record")) - # Row K: SessionMemoryNode write then erase - passes.append(_check("K β€” SessionMemoryNode write+erase executed (GDPR Art. 17)", + passes.append(_check("K - SessionMemoryNode write+erase executed (GDPR Art. 17)", fs.get("memory_erased") is True, f"erased subject: {fs.get('memory_erased_subject')}")) - # Row L: CredentialVault.get_with_trust audit_trail = result.get("credential_audit_trail") or [] - trust_gated = any(e.get("trust_level") for e in audit_trail) - passes.append(_check("L β€” CredentialVault.get_with_trust() called (Art. 15(4))", - trust_gated or len(audit_trail) > 0, + passes.append(_check("L - CredentialVault.get_with_trust() called (Art. 15(5))", + any(e.get("trust_level") for e in audit_trail) or len(audit_trail) > 0, f"credential access events: {len(audit_trail)}")) - # ── Summary ─────────────────────────────────────────────────────────────── - total = len(passes) - passed = sum(passes) - console.print(f"\n[bold]{'─'*60}[/bold]") + # M: Art. 27 FRIA β€” applicable for FINANCE (credit scoring) and complete. + fria = result.get("fria_art27") or {} + m_ok = ( + fria.get("applicable") is True + and fria.get("completeness", {}).get("complete") is True + and result.get("fria_art27_path") is not None + ) + passes.append(_check("M - Article27FRIANode: deployer FRIA generated & complete (Art. 27)", + m_ok, + f"applicable={fria.get('applicable')}, " + f"complete={fria.get('completeness', {}).get('complete')}, " + f"file={result.get('fria_art27_path')}")) + + # N: Omnibus Art. 5 additions present in the guard. + try: + from lar.compliance import ProhibitedPracticeGuard + _g = ProhibitedPracticeGuard(input_key="x") + n_ok = "NCII" in _g.heuristics and "CSAM" in _g.heuristics + except Exception: + n_ok = False + passes.append(_check("N - ProhibitedPracticeGuard covers 2 Dec 2026 NCII/CSAM bans (Art. 5)", + n_ok)) + + # O: both Art. 50 markings applied. + o_ok = ( + isinstance(fs.get("final_output"), str) + and "Art. 50(1)/(4)" in fs.get("final_output", "") + and isinstance(fs.get("final_output_marked"), dict) + and "c2pa_manifest" in fs.get("final_output_marked", {}) + ) + passes.append(_check("O - SyntheticMarker VISIBLE (50(1)/(4)) + METADATA (50(2)) both applied", + o_ok)) + + total, passed = len(passes), sum(passes) + console.print(f"\n[bold]{'-'*60}[/bold]") if passed == total: - console.print( - f"[bold green]βœ“ All {passed}/{total} runtime checks passed β€” " - f"23 paper-mapped requirements verified (v2.2.0).[/bold green]" - ) + console.print(f"[bold green]OK - all {passed}/{total} runtime checks passed.[/bold green]") else: - console.print( - f"[bold red]βœ— {passed}/{total} checks passed β€” " - f"{total - passed} requirement(s) need attention.[/bold red]" - ) + console.print(f"[bold red]FAIL - {passed}/{total} passed; " + f"{total - passed} need attention.[/bold red]") console.print( - "\n[dim]See docs/compliance/paper-compliance-mapping.md for full " - "Nannini et al. (2026) ↔ LΓ‘r primitive mapping.[/dim]" + "\n[dim]FRIA artifact: enterprise_audit/fria_art27.md | " + "Full mapping: docs/compliance/paper-compliance-mapping.md[/dim]" ) + return passed == total if __name__ == "__main__": - main() + import sys + sys.exit(0 if main() else 1) diff --git a/src/lar/compliance/__init__.py b/src/lar/compliance/__init__.py index d8d8925..65ec208 100644 --- a/src/lar/compliance/__init__.py +++ b/src/lar/compliance/__init__.py @@ -15,6 +15,7 @@ # v2.2.0 β€” EU AI Act gap-closure primitives from .fria_node import FundamentalRightsImpactNode, FRIAViolation +from .fria_art27_node import Article27FRIANode, Art27FRIAIncompleteError from .session_memory_node import SessionMemoryNode from .supplier_agreement_registry import SupplierAgreementRegistry, AgreementNotFoundError from .deployer_transparency_node import DeployerTransparencyNode @@ -42,6 +43,7 @@ "ProhibitedPracticeGuard", "ProhibitedPracticeError", # v2.2.0 gap-closure nodes "FundamentalRightsImpactNode", "FRIAViolation", + "Article27FRIANode", "Art27FRIAIncompleteError", "SessionMemoryNode", "SupplierAgreementRegistry", "AgreementNotFoundError", "DeployerTransparencyNode", diff --git a/src/lar/compliance/bias_filter.py b/src/lar/compliance/bias_filter.py index e2c926c..241301e 100644 --- a/src/lar/compliance/bias_filter.py +++ b/src/lar/compliance/bias_filter.py @@ -4,10 +4,15 @@ class BiasFilterNode(BaseNode): """ - Evaluates state variables against fairness criteria to detect potential bias. - If bias is detected (simulated here via keyword heuristics or a threshold), - it routes to a human jury for oversight. - Operationalizes prEN 18283 (Bias Management). + Runtime bias **keyword gate**: scans a text output for protected-characteristic + terms and, on a hit, routes to a human jury for oversight. + + Scope of the claim: this is a lightweight detection aid that *supports* β€” it + does **not** by itself satisfy β€” the Art. 10(2)(f)-(g) "examination in view of + possible biases" obligation or conformance to prEN 18283 (bias management). + A substring match over-triggers (e.g. "regardless of age or gender") and + under-triggers (bias expressed in neutral wording). Treat a clean pass as + "no obvious protected term surfaced", not "the decision is unbiased". """ def __init__(self, input_key: str, diff --git a/src/lar/compliance/credential_vault.py b/src/lar/compliance/credential_vault.py index 550c4bc..5adb7ae 100644 --- a/src/lar/compliance/credential_vault.py +++ b/src/lar/compliance/credential_vault.py @@ -10,7 +10,19 @@ class CredentialVault: """ Just-in-Time (JIT) credential provisioning for agent tools. - Operationalises EU AI Act Art. 15(4) and GDPR (NHI Governance / Least Privilege). + + Supports EU AI Act **Art. 15(5)** (cybersecurity β€” resilience against + unauthorised third parties exploiting system vulnerabilities) by keeping the + generative model out of the credential path: Non-Human Identity (NHI) least + privilege is applied as a security measure "appropriate to the relevant + circumstances and the risks" (Art. 15(5), first subparagraph). Note that + least privilege is not verbatim Art. 15(5) text β€” it is a proportionate + technical measure that supports the Article, aligned with prEN 18282 + (AI cybersecurity) and the Cyber Resilience Act's least-privilege principle. + + (Earlier versions cited "Art. 15(4)"; in Regulation (EU) 2024/1689 as + enacted, 15(4) is robustness / error-resilience / feedback loops and + **15(5)** is cybersecurity. Corrected repo-wide.) v2.2.0 additions: - ``_audit_trail``: per-action NHI authorisation log (iterable for compliance export) @@ -19,10 +31,14 @@ class CredentialVault: threshold. Enables deployers to restrict sensitive credentials to high-trust execution paths (e.g., only grant payment API key after HumanJuryNode approves). - EU Reference: Art. 15(4) EU AI Act β€” Cybersecurity; NHI Governance / Least Privilege + EU Reference: Art. 15(5) EU AI Act β€” Cybersecurity; NHI least privilege + (proportionate security measure); prEN 18282. """ - EU_REFERENCE = "Art. 15(4) EU AI Act β€” Cybersecurity; NHI Governance / Least Privilege" + EU_REFERENCE = ( + "Art. 15(5) EU AI Act β€” Cybersecurity (resilience against unauthorised " + "third parties); NHI least privilege as a proportionate security measure; prEN 18282" + ) def __init__(self, logger_callback=None): self._vault: Dict[str, str] = {} @@ -105,7 +121,7 @@ def get_with_trust( raise PermissionError( f"[CredentialVault] Credential '{credential_key}' requires trust_level " f"'{min_level}' but calling context has '{trust_level}'. " - f"Art. 15(4) privilege restriction active." + f"Art. 15(5) privilege restriction active." ) self._log_access(tool_name, scope, credential_key, "GRANTED", trust_level) diff --git a/src/lar/compliance/fria_art27_node.py b/src/lar/compliance/fria_art27_node.py new file mode 100644 index 0000000..c3a26d1 --- /dev/null +++ b/src/lar/compliance/fria_art27_node.py @@ -0,0 +1,280 @@ +""" +lar.compliance.fria_art27_node +============================== +Article27FRIANode β€” the **Article 27 Fundamental Rights Impact Assessment** +(a *deployer* obligation), as a structured, machine-readable template generator. + +This is the Act's named, standalone FRIA β€” distinct from +``FundamentalRightsImpactNode`` (a runtime Art. 9(2)(a) output-screening heuristic +that is explicitly *not* the Art. 27 FRIA). + +Who must do it β€” Art. 27(1). Only these deployers of Annex III high-risk systems +(and NOT for Annex III point 2, critical infrastructure): + * bodies governed by public law; + * private entities providing public services; + * any deployer using a system for creditworthiness evaluation / credit scoring + (Annex III 5(b)); or for risk assessment and pricing in life and health + insurance (Annex III 5(c)). + +What it must contain β€” Art. 27(1)(a)-(f): + (a) the deployer's processes in which the system will be used, in line with its + intended purpose; + (b) the period of time and frequency of intended use; + (c) the categories of natural persons and groups likely to be affected; + (d) the specific risks of harm to those categories, taking into account the + information the provider gave under Art. 13; + (e) the implementation of human oversight measures, per the instructions for use; + (f) the measures to take if those risks materialise, including internal + governance arrangements and complaint mechanisms. + +Also modelled: + * Art. 27(3) β€” once performed, the deployer notifies the market surveillance + authority of the results (via the AI Office template, Art. 27(5)). + * Art. 27(4), as reinforced by the Digital Omnibus (Regulation (EU) 2026/1744) + β€” the FRIA may incorporate or cross-refer relevant parts of a GDPR Art. 35 + Data Protection Impact Assessment rather than duplicating them. + +LΓ‘r generates the template and checks completeness; the deployer supplies the +substantive content and files it with the authority. Using this node is not the +assessment. +""" + +from __future__ import annotations + +import datetime +from typing import List, Optional + +from lar.node import BaseNode +from lar.state import GraphState + + +# Deployer classes that are IN SCOPE for Art. 27. +IN_SCOPE_DEPLOYER_CLASSES = ( + "PUBLIC_BODY", + "PRIVATE_PUBLIC_SERVICE", + "CREDIT_SCORING", # Annex III 5(b) + "LIFE_HEALTH_INSURANCE", # Annex III 5(c) +) +OUT_OF_SCOPE_DEPLOYER_CLASSES = ("OUT_OF_SCOPE", "NONE", None) + +_DEFAULT_CHARTER_RIGHTS = [ + "Dignity (Art. 1)", + "Respect for private and family life (Art. 7)", + "Protection of personal data (Art. 8)", + "Freedom of expression and information (Art. 11)", + "Non-discrimination (Art. 21)", + "Rights of the child (Art. 24)", + "Rights of the elderly (Art. 25)", + "Integration of persons with disabilities (Art. 26)", + "Consumer protection (Art. 38)", + "Right to an effective remedy and to a fair trial (Art. 47)", +] + + +class Art27FRIAIncompleteError(Exception): + """Raised in strict mode when an in-scope FRIA is missing a required element.""" + pass + + +class Article27FRIANode(BaseNode): + """ + Generates an Art. 27 Fundamental Rights Impact Assessment record. + + On ``execute`` it writes ``state[output_key]`` (default ``"fria_art27"``): + * out of scope β†’ ``{"applicable": False, "reason": ...}`` + * in scope β†’ the full structured FRIA dict, plus + ``state["fria_art27_notify_authority_required"] = True``. + + EU Reference: Art. 27 EU AI Act β€” Fundamental Rights Impact Assessment + (deployer obligation); Art. 27(3) authority notification; Art. 27(4) + + Regulation (EU) 2026/1744 DPIA cross-reference. + """ + + EU_REFERENCE = ( + "Art. 27 EU AI Act β€” Fundamental Rights Impact Assessment (deployer " + "obligation); Art. 27(3) authority notification; Art. 27(4) / " + "Regulation (EU) 2026/1744 β€” may cross-refer a GDPR Art. 35 DPIA" + ) + + _REQUIRED_ELEMENTS = ( + ("deployment_process", "27(1)(a) deployer processes / intended-purpose use"), + ("usage_period_and_frequency", "27(1)(b) period of time and frequency of use"), + ("affected_natural_persons", "27(1)(c) categories of affected natural persons/groups"), + ("fundamental_rights_risks", "27(1)(d) specific risks of harm (informed by Art. 13 info)"), + ("human_oversight_measures", "27(1)(e) human oversight measures per instructions for use"), + ("measures_on_materialisation", "27(1)(f) measures on materialisation + governance + complaints"), + ) + + def __init__( + self, + system_name: str, + deployer_class: Optional[str], + intended_purpose: str = "", + annex_iii_point: str = "", + deployment_process: str = "", + usage_period_and_frequency: str = "", + affected_natural_persons: Optional[List[str]] = None, + fundamental_rights_risks: Optional[List[str]] = None, + human_oversight_measures: Optional[List[str]] = None, + measures_on_materialisation: Optional[List[str]] = None, + provider_info_reference: Optional[str] = None, + dpia_reference: Optional[str] = None, + charter_rights_considered: Optional[List[str]] = None, + output_key: str = "fria_art27", + strict: bool = False, + next_node: Optional[BaseNode] = None, + ): + """ + Args: + deployer_class: one of ``PUBLIC_BODY | PRIVATE_PUBLIC_SERVICE | + CREDIT_SCORING | LIFE_HEALTH_INSURANCE`` (in scope) or + ``OUT_OF_SCOPE`` / ``None``. + strict: if True and the deployer is in scope, raise + ``Art27FRIAIncompleteError`` when any 27(1)(a)-(f) element is empty. + """ + self.system_name = system_name + self.deployer_class = deployer_class + self.intended_purpose = intended_purpose + self.annex_iii_point = annex_iii_point + self.deployment_process = deployment_process + self.usage_period_and_frequency = usage_period_and_frequency + self.affected_natural_persons = affected_natural_persons or [] + self.fundamental_rights_risks = fundamental_rights_risks or [] + self.human_oversight_measures = human_oversight_measures or [] + self.measures_on_materialisation = measures_on_materialisation or [] + self.provider_info_reference = provider_info_reference + self.dpia_reference = dpia_reference + self.charter_rights_considered = charter_rights_considered or list(_DEFAULT_CHARTER_RIGHTS) + self.output_key = output_key + self.strict = strict + self.next_node = next_node + + # ── Scope ──────────────────────────────────────────────────────────────── + + def in_scope(self) -> bool: + return self.deployer_class in IN_SCOPE_DEPLOYER_CLASSES + + # ── Execute ────────────────────────────────────────────────────────────── + + def execute(self, state: GraphState) -> Optional[BaseNode]: + if not self.in_scope(): + record = { + "schema": "lar-art27-fria-v1", + "applicable": False, + "deployer_class": self.deployer_class, + "reason": ( + "Art. 27 applies only to deployers that are public bodies, " + "private providers of public services, or deployers doing " + "credit scoring (Annex III 5(b)) / life & health insurance " + "risk pricing (Annex III 5(c)). This deployer class is not " + "in that set." + ), + "eu_reference": self.EU_REFERENCE, + } + state.set(self.output_key, record) + print( + f" [Article27FRIANode] deployer_class='{self.deployer_class}' β€” " + f"Art. 27 FRIA not applicable; recorded rationale." + ) + return self.next_node + + missing = [ + desc for attr, desc in self._REQUIRED_ELEMENTS + if not getattr(self, attr) + ] + + record = { + "schema": "lar-art27-fria-v1", + "applicable": True, + "generated_at": datetime.datetime.utcnow().isoformat() + "Z", + "eu_reference": self.EU_REFERENCE, + "system_name": self.system_name, + "deployer_class": self.deployer_class, + "intended_purpose": self.intended_purpose, + "annex_iii_point": self.annex_iii_point, + # Art. 27(1)(a)-(f) + "a_deployment_process": self.deployment_process, + "b_usage_period_and_frequency": self.usage_period_and_frequency, + "c_affected_natural_persons": self.affected_natural_persons, + "d_fundamental_rights_risks": self.fundamental_rights_risks, + "d_provider_info_reference_art13": self.provider_info_reference, + "e_human_oversight_measures": self.human_oversight_measures, + "f_measures_on_materialisation": self.measures_on_materialisation, + # supporting / procedural + "charter_rights_considered": self.charter_rights_considered, + "dpia_reference_art27_4": self.dpia_reference, + "notify_market_surveillance_authority": { + "required": True, + "basis": "Art. 27(3) β€” notify the authority of the results via the AI Office template (Art. 27(5))", + }, + "completeness": { + "complete": not missing, + "missing_elements": missing, + }, + } + state.set(self.output_key, record) + state.set("fria_art27_notify_authority_required", True) + + status = "COMPLETE" if not missing else f"INCOMPLETE β€” missing {len(missing)} element(s)" + print( + f" [Article27FRIANode] Art. 27 FRIA generated for '{self.system_name}' " + f"(deployer_class={self.deployer_class}) β†’ state['{self.output_key}'] [{status}]" + ) + if missing: + for m in missing: + print(f" Β· missing: {m}") + if self.strict: + raise Art27FRIAIncompleteError( + f"Art. 27 FRIA for '{self.system_name}' is missing required " + f"element(s): {missing}" + ) + return self.next_node + + # ── Human-readable export ──────────────────────────────────────────────── + + def as_markdown(self, state: GraphState) -> str: + d = state.get(self.output_key) or {} + if not d.get("applicable", False): + return ( + f"# Article 27 FRIA β€” Not Applicable\n\n" + f"**System:** {self.system_name}\n\n" + f"**Deployer class:** {d.get('deployer_class')}\n\n" + f"{d.get('reason', '')}\n" + ) + lines = [ + f"# Fundamental Rights Impact Assessment (EU AI Act Article 27)", + f"**System:** {d.get('system_name')}", + f"**Deployer class:** {d.get('deployer_class')}", + f"**Annex III point:** {d.get('annex_iii_point') or 'β€”'}", + f"**Generated:** {d.get('generated_at')}", + f"**EU reference:** {d.get('eu_reference')}", + "", + "## (a) Deployer processes / intended-purpose use", + d.get("a_deployment_process") or "_[to be completed by deployer]_", + "", + "## (b) Period of time and frequency of use", + d.get("b_usage_period_and_frequency") or "_[to be completed by deployer]_", + "", + "## (c) Categories of affected natural persons and groups", + ] + lines += [f"- {p}" for p in d.get("c_affected_natural_persons", [])] or ["_[to be completed]_"] + lines += ["", "## (d) Specific risks of harm (informed by Art. 13 provider information)"] + lines += [f"- {r}" for r in d.get("d_fundamental_rights_risks", [])] or ["_[to be completed]_"] + if d.get("d_provider_info_reference_art13"): + lines += ["", f"_Provider information relied on:_ {d['d_provider_info_reference_art13']}"] + lines += ["", "## (e) Human oversight measures (per instructions for use)"] + lines += [f"- {m}" for m in d.get("e_human_oversight_measures", [])] or ["_[to be completed]_"] + lines += ["", "## (f) Measures on materialisation (incl. internal governance & complaint mechanisms)"] + lines += [f"- {m}" for m in d.get("f_measures_on_materialisation", [])] or ["_[to be completed]_"] + lines += ["", "## Charter rights considered"] + lines += [f"- {r}" for r in d.get("charter_rights_considered", [])] + lines += [ + "", + "## Procedural", + f"- **DPIA cross-reference (Art. 27(4)):** {d.get('dpia_reference_art27_4') or 'none provided'}", + f"- **Authority notification (Art. 27(3)):** required β€” file the AI Office template with the market surveillance authority.", + ] + comp = d.get("completeness", {}) + if not comp.get("complete", False): + lines += ["", f"> ⚠ INCOMPLETE β€” missing: {comp.get('missing_elements')}"] + return "\n".join(lines) diff --git a/src/lar/compliance/incident_reporter.py b/src/lar/compliance/incident_reporter.py index 71ab180..15ba13e 100644 --- a/src/lar/compliance/incident_reporter.py +++ b/src/lar/compliance/incident_reporter.py @@ -16,11 +16,14 @@ class IncidentReporter: """ - Aggregates runtime execution logs and Authority Ledger records to produce - a Post-Market Monitoring (PMM) report. - - Operationalises EU AI Act Art. 72 (Post-Market Monitoring) and provides - evidence for ISO 9001 Clause 9 (Performance Evaluation). + Aggregates runtime execution logs and Authority Ledger records into a + Post-Market Monitoring (PMM) evidence summary. + + Scope: this **supports** EU AI Act Art. 72 (post-market monitoring) by + collecting the operational data a PMM system consumes. It does not replace + the documented **post-market monitoring plan** (Art. 72(3) β€” template set by + Commission implementing act) or the provider's active data-collection duties. + Also useful as evidence for ISO 9001 Clause 9 (Performance Evaluation). """ def __init__(self, log_dir: str): @@ -156,33 +159,45 @@ class IncidentReporterNode: LΓ‘r has no way to automatically determine from a generic harm signal whether an incident is legally "widespread" or involved a death β€” that is - a factual/legal determination, not a severity score. DEADLINE_HOURS below - is therefore a conservative HEURISTIC mapping from LΓ‘r's own internal - severity tiers onto the real Art. 73 deadlines (fastest deadline for the - most severe automated triggers, general 15-day default otherwise) β€” it is - not itself a legal classification. Confirm the applicable paragraph against - the actual incident facts before relying on any deadline_by value produced - here. + a factual/legal determination, not a severity score. ``DEADLINE_HOURS`` + below is therefore a **conservative ceiling**: LΓ‘r's more severe automated + triggers are pinned to the *fastest* Art. 73 deadline (2 days) so a provider + who acts on the LΓ‘r value is never late, and everything else to the 15-day + general default. The real per-paragraph deadlines are attached to every + record as ``art_73_legal_deadlines`` and the record is flagged + ``provider_must_confirm_applicable_paragraph``. LΓ‘r deliberately does NOT + emit the Art. 73(4) "death of a person" 10-day deadline off a generic + severity tier β€” mapping an unclassified signal onto that paragraph would be + a false legal classification. - DEADLINE_HOURS: - CRITICAL β†’ 48 h (Art. 73(3) deadline, applied as a fail-safe ceiling for - LΓ‘r's most severe automated triggers) - HIGH β†’ 240 h (Art. 73(4) deadline) - MEDIUM β†’ 360 h (Art. 73(2) general default) - LOW β†’ None (below LΓ‘r's own reportability threshold β€” not an - Art. 73 category; not a claim that the Act itself - exempts these from reporting) + DEADLINE_HOURS (conservative ceiling β€” not a legal classification): + CRITICAL β†’ 48 h (Art. 73(3) β€” fastest deadline, applied as a fail-safe) + HIGH β†’ 48 h (same fail-safe ceiling β€” do not assume 73(4)/death) + MEDIUM β†’ 360 h (Art. 73(2) general default β€” 15 days) + LOW β†’ None (below LΓ‘r's own reportability threshold β€” NOT a claim + that the Act exempts these from reporting) """ - EU_REFERENCE = "Art. 73 EU AI Act β€” Serious Incident Reporting (heuristic severity mapping β€” see class docstring)" + EU_REFERENCE = ( + "Art. 73 EU AI Act β€” Serious Incident Reporting. Deadline shown is a " + "conservative ceiling, not a legal classification β€” see art_73_legal_deadlines." + ) DEADLINE_HOURS: Dict[str, Optional[int]] = { "CRITICAL": 48, - "HIGH": 240, + "HIGH": 48, "MEDIUM": 360, "LOW": None, } + # The actual Art. 73 deadlines, keyed by INCIDENT TYPE (a factual/legal + # determination the provider must make β€” LΓ‘r cannot infer it). + ART_73_LEGAL_DEADLINES: Dict[str, str] = { + "73(2)_general_default": "15 days (360 h)", + "73(3)_widespread_infringement_or_serious_incident_3_49_b": "2 days (48 h)", + "73(4)_death_of_a_person": "10 days (240 h)", + } + # Heuristic harm signals in graph state _HARM_KEYS = { "last_error": "RUNTIME_ERROR", @@ -305,11 +320,14 @@ def _write_incident(self, details: Dict) -> None: severity = details.get("severity", "MEDIUM") deadline_h = self.DEADLINE_HOURS.get(severity) record = { - "schema": "lar-incident-v1", + "schema": "lar-incident-v2", "eu_reference": self.EU_REFERENCE, "reported_at": now, "severity": severity, "reporting_deadline_hours": deadline_h, + "reporting_deadline_is_conservative_ceiling": True, + "provider_must_confirm_applicable_paragraph": True, + "art_73_legal_deadlines": self.ART_73_LEGAL_DEADLINES, "deadline_by": ( (datetime.datetime.utcnow() + datetime.timedelta(hours=deadline_h)).isoformat() + "Z" if deadline_h else None diff --git a/src/lar/compliance/manifest.py b/src/lar/compliance/manifest.py index e6cde08..50ba27d 100644 --- a/src/lar/compliance/manifest.py +++ b/src/lar/compliance/manifest.py @@ -138,7 +138,7 @@ def _visit(self, node: Any) -> List[Dict]: "jit_credential_vault_attached": vault is not None, "credential_key": getattr(node, "credential_key", None), "eu_act_relevance": { - "article": "Art. 15(4) (Cybersecurity/Privilege), Art. 14 (Oversight)", + "article": "Art. 15(5) (Cybersecurity), Art. 14 (Oversight)", "note": ( "EXTERNAL ACTION β€” activates regulatory triggers. " f"Affects: {affected}. " @@ -181,11 +181,13 @@ def _visit(self, node: Any) -> List[Dict]: "parallel_branch_count": len(sub_nodes), "parallel_node_types": [n.__class__.__name__ for n in sub_nodes], "eu_act_relevance": { - "article": "Art. 9 (Risk Management), prEN 18229 (Trustworthiness)", + "article": "Art. 15(4) (Robustness β€” state isolation prevents error propagation), prEN 18229-2", "note": ( f"Launches {len(sub_nodes)} isolated parallel sub-agents. " "Each sub-agent runs in a cloned, isolated GraphState. " - "Results are merged via explicit reducer logic β€” no context bleed." + "Results are merged via explicit reducer logic β€” no context bleed. " + "State isolation is a robustness measure (Art. 15(4) error-resilience), " + "not a cybersecurity control." ) } } @@ -243,8 +245,12 @@ def _visit(self, node: Any) -> List[Dict]: entry = { "node_type": "BiasFilterNode", "eu_act_relevance": { - "article": "prEN 18283 (Bias Management), Art. 10(2)(f-g)", - "note": "Bias heuristic check before output. Escalates to HumanJury if bias detected." + "article": "Art. 10(2)(f)-(g) (examination for bias); prEN 18283 (supporting standard)", + "note": ( + "Runtime bias keyword gate before output; escalates to HumanJury on a hit. " + "This is a lightweight detection aid β€” it supports, but does not by itself " + "satisfy, the Art. 10 data-governance obligation or prEN 18283 conformance." + ) } } entries.append(entry) @@ -268,7 +274,7 @@ def _visit(self, node: Any) -> List[Dict]: affected = meta.get("affected_parties", "USER_ONLY") is_external = meta.get("external_action", False) if is_external: - article = "Art. 15(4) (Cybersecurity/Privilege), Art. 14 (Oversight)" + article = "Art. 15(5) (Cybersecurity), Art. 14 (Oversight)" if affected in ("THIRD_PARTY", "BOTH"): article += ", Art. 50 (Transparency to third parties)" note = meta.get("description", f"External action node '{node_type}'.") @@ -376,7 +382,11 @@ def generate(self) -> Dict: "system_name": self.system_name, "domain": self.domain, "generated_at": datetime.datetime.utcnow().isoformat() + "Z", - "eu_ai_act_reference": "Step 9 β€” External Action Inventory (Section 8.1, Nannini et al., 2026)", + "eu_ai_act_reference": ( + "External action inventory β€” Nannini et al. (2026) Step 9 (Section 8.1); " + "feeds Annex IV Β§2(b) (description of components and their interactions). " + "The full Annex IV technical documentation remains a provider deliverable." + ), "summary": { "total_nodes_inventoried": len(inventory), "node_type_breakdown": node_type_counts, @@ -404,7 +414,7 @@ def _compute_risk_flags( if unvaulted_tools: flags.append({ "severity": "HIGH", - "article": "Art. 15(4) / prEN 18282", + "article": "Art. 15(5) / prEN 18282", "message": ( f"{len(unvaulted_tools)} ToolNode(s) execute without a CredentialVault. " "This violates the privilege minimization requirement. " diff --git a/src/lar/compliance/prohibited_practice_guard.py b/src/lar/compliance/prohibited_practice_guard.py index b7b3009..8bbfce3 100644 --- a/src/lar/compliance/prohibited_practice_guard.py +++ b/src/lar/compliance/prohibited_practice_guard.py @@ -9,53 +9,103 @@ class ProhibitedPracticeError(Exception): class ProhibitedPracticeGuard(BaseNode): """ - A final-stage belt-and-suspenders guard that scans AI output for language - indicating prohibited practices under EU AI Act Art. 5 (effective Feb 2025). - - This includes: - 1. Subliminal, manipulative, or deceptive techniques. - 2. Exploitation of vulnerabilities (age, disability, social/economic situation). - 3. Social scoring (evaluating trustworthiness based on social behaviour). + A final-stage belt-and-suspenders guard that scans AI output for language + indicating a prohibited practice under EU AI Act Art. 5. + + Core Art. 5(1) bans (in force since 2 February 2025): + 1. Subliminal / manipulative / deceptive techniques (Art. 5(1)(a)) + 2. Exploitation of vulnerabilities β€” age, disability, socio-economic + situation (Art. 5(1)(b)) + 3. Social scoring / trustworthiness evaluation from social behaviour + (Art. 5(1)(c)) + + Digital Omnibus (Regulation (EU) 2026/1744) additions, applicable + **2 December 2026**: + 4. AI systems designed or used to produce non-consensual intimate + (sexually explicit) imagery of real, identifiable persons (NCII) + 5. AI systems designed or used to produce child sexual abuse + material (CSAM) + + These heuristics are a lightweight last line of defence β€” a substring scan, + not a classifier. Pair with an upstream input filter and, for a real + deployment, an LLM-as-judge or a dedicated safety classifier. A clean pass + means "no flagged phrasing surfaced", not "Art. 5 compliant". """ - def __init__(self, - input_key: str, + + # Categories whose legal basis only begins to apply on 2 Dec 2026. + OMNIBUS_CATEGORIES = ("NCII", "CSAM") + + def __init__(self, + input_key: str, next_node: Optional[BaseNode] = None, - block_on_violation: bool = True): + block_on_violation: bool = True, + include_omnibus_categories: bool = True): + """ + Args: + input_key: state key holding the text to scan. + next_node: node to continue to when clean. + block_on_violation: raise ``ProhibitedPracticeError`` on any hit. + include_omnibus_categories: also scan for the 2 Dec 2026 NCII/CSAM + additions (default True; set False to scan only the pre-2026 bans). + """ self.input_key = input_key self.next_node = next_node self.block_on_violation = block_on_violation - + self.include_omnibus_categories = include_omnibus_categories + # Simple heuristic regexes to detect risky phrasing in the final output. - # In a full deployment, this might be supplemented with an LLM-as-a-judge. self.heuristics = { - "SOCIAL_SCORING": re.compile(r'\b(social credit|trustworthiness score|social standing|behaviour score)\b', re.IGNORECASE), - "MANIPULATION": re.compile(r'\b(must act now or|secretly track|coerce|exploit vulnerability|subliminal)\b', re.IGNORECASE), - "VULNERABILITY_EXPLOIT": re.compile(r'\b(target elderly|target minors|leverage desperation)\b', re.IGNORECASE) + "SOCIAL_SCORING": re.compile( + r'\b(social credit|trustworthiness score|social standing|behaviour score)\b', + re.IGNORECASE), + "MANIPULATION": re.compile( + r'\b(must act now or|secretly track|coerce|exploit vulnerability|subliminal)\b', + re.IGNORECASE), + "VULNERABILITY_EXPLOIT": re.compile( + r'\b(target elderly|target minors|leverage desperation)\b', + re.IGNORECASE), } + if include_omnibus_categories: + self.heuristics["NCII"] = re.compile( + r'\b(non[- ]consensual (intimate|sexual|explicit)|deepfake (nude|porn)|' + r'undress(ed|ing)? (image|photo|picture)|sexually explicit (image|deepfake) ' + r'of (a|an|the) (real|identifiable|named) (person|individual))\b', + re.IGNORECASE) + self.heuristics["CSAM"] = re.compile( + r'\b(child sexual abuse material|csam|sexuali[sz]ed (image|depiction|content) ' + r'of a (child|minor)|minor in a sexual)\b', + re.IGNORECASE) def execute(self, state: GraphState) -> Optional[BaseNode]: content = str(state.get(self.input_key, "")) if not content: return self.next_node - + violations = [] for risk_category, pattern in self.heuristics.items(): if pattern.search(content): violations.append(risk_category) - + if violations: + omnibus = [v for v in violations if v in self.OMNIBUS_CATEGORIES] + basis = "Art. 5(1) EU AI Act" + if omnibus: + basis += ( + f" (+ Regulation (EU) 2026/1744 additions {omnibus}, " + f"applicable 2 Dec 2026)" + ) msg = ( - f"[ProhibitedPracticeGuard] EU AI ACT ART 5 VIOLATION DETECTED.\n" + f"[ProhibitedPracticeGuard] {basis} β€” PROHIBITED PRACTICE DETECTED.\n" f"Flagged categories: {violations}\n" f"Output key scanned: '{self.input_key}'" ) state.set("_prohibited_practice_flag", violations) - + print(f"\n{'!'*60}") print(msg) print(f"{'!'*60}\n") - + if self.block_on_violation: raise ProhibitedPracticeError(msg) - + return self.next_node diff --git a/src/lar/compliance/supplier_agreement_registry.py b/src/lar/compliance/supplier_agreement_registry.py index be7b3aa..989aeba 100644 --- a/src/lar/compliance/supplier_agreement_registry.py +++ b/src/lar/compliance/supplier_agreement_registry.py @@ -3,10 +3,19 @@ ========================================== SupplierAgreementRegistry β€” Art. 25(4) Written Agreement Enforcement. -EU AI Act Art. 25(4): Where a deployer uses an AI system provided by a provider, -the parties must define by written agreement which obligations fall on whom. -Before a ToolNode executes a third-party tool, the registry verifies that a -current, unexpired written agreement exists. +EU AI Act Art. 25(4): the provider of a high-risk AI system and any third party +supplying tools, services, components or processes integrated into it must +specify, by written agreement, the information / capabilities / technical access +needed for the provider to comply with the Regulation. + +FOSS carve-out: Art. 25(4) explicitly does **not** apply to third parties making +tools/services/components (other than GPAI models) available under a free and +open-source licence. LΓ‘r itself (Apache-2.0, not a GPAI model) is therefore +outside the scope of Art. 25(4) as a supplied component. This registry exists +for the customer-provider's *other* suppliers β€” the non-FOSS APIs, model +gateways, and data services their high-risk system integrates. Before a ToolNode +executes such a third-party tool, the registry verifies a current, unexpired +written agreement exists. Wire this into ToolNode via the ``pre_execute_hook`` pattern, or call ``registry.assert_agreement(tool_name)`` directly from tool functions. diff --git a/src/lar/compliance/synthetic_marker.py b/src/lar/compliance/synthetic_marker.py index c616de6..dadf20f 100644 --- a/src/lar/compliance/synthetic_marker.py +++ b/src/lar/compliance/synthetic_marker.py @@ -3,56 +3,110 @@ from lar.node import BaseNode from lar.state import GraphState +try: + from lar import __version__ as _LAR_VERSION +except Exception: # pragma: no cover - version import is best-effort + _LAR_VERSION = "unknown" + + class SyntheticMarkerNode(BaseNode): """ - Injects a machine-readable watermark or visible disclaimer into generated content. - Operationalizes Article 50(2) transparency obligations for synthetic content. + Marks AI-generated content per the Art. 50 transparency regime. + + Two distinct obligations, selected by ``marker_type``: + + * ``METADATA`` β†’ **Art. 50(2)** β€” providers of AI systems generating synthetic + audio/image/video/text must mark outputs in a **machine-readable** format, + detectable as artificially generated (effective, interoperable, robust "as + far as technically feasible"). This path writes a simulated C2PA manifest. + * ``VISIBLE`` β†’ **Art. 50(1)** (AI-interaction disclosure to natural persons) + and **Art. 50(4)** (deep fakes / AI-generated text published to inform the + public on matters of public interest) β€” a human-readable disclosure. This + path appends a plain-language disclaimer. + + A production deployment usually needs BOTH: run one node with ``METADATA`` and + one with ``VISIBLE``. + + Timeline note: Art. 50 applies from 2 August 2026 and was **not** amended by + the Digital Omnibus (Regulation (EU) 2026/1744). Providers of generative AI + systems already on the market have until **2 December 2026** to implement the + Art. 50(2) machine-readable marking duty. """ - def __init__(self, - input_key: str, + + EU_REFERENCE_METADATA = ( + "Art. 50(2) EU AI Act β€” machine-readable marking of synthetic content " + "(providers of generative AI systems)" + ) + EU_REFERENCE_VISIBLE = ( + "Art. 50(1)/(4) EU AI Act β€” human-readable AI disclosure to affected " + "natural persons / deep-fake & public-interest text labelling" + ) + + def __init__(self, + input_key: str, output_key: Optional[str] = None, - marker_type: str = "VISIBLE", + marker_type: str = "VISIBLE", next_node: Optional[BaseNode] = None): """ Args: input_key: The state key containing the content to mark. output_key: The state key to save marked content (defaults to overwriting input_key). - marker_type: "VISIBLE" (text disclaimer) or "METADATA" (simulated C2PA manifest). + marker_type: "VISIBLE" (Art. 50(1)/(4) text disclaimer) or + "METADATA" (Art. 50(2) machine-readable / simulated C2PA manifest). """ self.input_key = input_key self.output_key = output_key or input_key self.marker_type = marker_type.upper() self.next_node = next_node + @property + def eu_reference(self) -> str: + return ( + self.EU_REFERENCE_METADATA + if self.marker_type == "METADATA" + else self.EU_REFERENCE_VISIBLE + ) + def execute(self, state: GraphState): content = state.get(self.input_key) if not content: return self.next_node timestamp = datetime.datetime.utcnow().isoformat() - + if self.marker_type == "VISIBLE": if isinstance(content, str): - marked_content = f"{content}\n\n[Disclaimer: This content was generated by an AI system on {timestamp}.]" + marked_content = ( + f"{content}\n\n[Disclaimer: This content was generated by an AI " + f"system on {timestamp}. (EU AI Act Art. 50(1)/(4).)]" + ) else: # If it's a dict, add a disclaimer field marked_content = content.copy() if isinstance(content, dict) else {"original": content} marked_content["_ai_disclaimer"] = f"Generated by AI on {timestamp}" - + marked_content["_ai_disclaimer_basis"] = self.EU_REFERENCE_VISIBLE + elif self.marker_type == "METADATA": - # Simulate a C2PA metadata wrapper + # Simulate a C2PA metadata wrapper (Art. 50(2) machine-readable marking). + # A production deployment substitutes a real C2PA / SynthID-class signer. marked_content = { "c2pa_manifest": { - "generator": "Lar Framework v1.5.1", + "generator": f"LΓ‘r {_LAR_VERSION}", "timestamp": timestamp, - "assertion": "AI_GENERATED" + "assertion": "AI_GENERATED", + "eu_reference": self.EU_REFERENCE_METADATA, + "note": "Simulated manifest β€” replace with a real C2PA signer in production.", }, - "content": content + "content": content, } else: marked_content = content - - print(f" [SyntheticMarkerNode] Applied {self.marker_type} marking to '{self.input_key}'.") + + state.set("synthetic_marker_reference", self.eu_reference) + print( + f" [SyntheticMarkerNode] Applied {self.marker_type} marking to " + f"'{self.input_key}' ({self.eu_reference.split(' β€” ')[0]})." + ) state.set(self.output_key, marked_content) - + return self.next_node diff --git a/src/lar/enterprise/backbone.py b/src/lar/enterprise/backbone.py index efd1c4e..c073bea 100644 --- a/src/lar/enterprise/backbone.py +++ b/src/lar/enterprise/backbone.py @@ -4,25 +4,45 @@ Reusable backbone that wires the compliance primitives into a single auditable graph. Drop in a DOMAIN_CONFIG dict to target any regulated vertical. -Paper coverage (Nannini et al., 2026 β€” all 23 requirements exercised at runtime): - Art. 9 β†’ PolicyRegistry + FundamentalRightsImpactNode (FRIA) - Art. 9 PMM β†’ BehavioralEnvelopeMonitor (output variance monitoring) +Requirement coverage. "Covered" = a runtime hook fires and/or an evidence +artifact is produced. It does NOT mean the full legal obligation is discharged β€” +conformity assessment, the QMS, and the substantive content of every assessment +remain the customer-provider's / deployer's responsibility. Citations are against +Regulation (EU) 2024/1689 as amended by the Digital Omnibus, Regulation (EU) +2026/1744 (in force 27 Jul 2026). + + Art. 9(2)(a) β†’ PolicyRegistry + FundamentalRightsImpactNode + (runtime fundamental-rights screening β€” NOT the Art. 27 FRIA) + Art. 9(9) β†’ BehavioralEnvelopeMonitor (output variance monitoring) Art. 12 β†’ AuditLogger (causal trace + verify_step_integrity + log_plan_switch) Art. 13 β†’ DeployerTransparencyNode (instructions for use) + TransparencyEngine - Art. 14 β†’ RiskScorerNode + HumanJuryNode (automation_boundary + decision_type) + Art. 14 β†’ RiskScorerNode + HumanJuryNode (automation_boundary=always_human + by default; a real decision is supplied via human_decision_provider) Art. 3(23) β†’ RuntimeStateVersioner + DriftDetector + DynamicToolDiscoveryMonitor - Art. 15(4) β†’ CredentialVault (get_with_trust β€” trust-based privilege) - Art. 25(4) β†’ SupplierAgreementRegistry (written agreement enforcement) - Art. 50(2) β†’ SyntheticMarkerNode (C2PA / visible disclaimer) - Art. 73-74 β†’ IncidentReporterNode (real-time incident detection + 24/72h deadlines) + Art. 15(5) β†’ CredentialVault (cybersecurity β€” NHI least privilege; get_with_trust) + Art. 25(4) β†’ SupplierAgreementRegistry (written agreement enforcement; note the + FOSS carve-out β€” LΓ‘r itself is outside Art. 25(4)) + Art. 27 β†’ Article27FRIANode (deployer Fundamental Rights Impact Assessment + template β€” gated on deployer_class; FINANCE/credit-scoring is in scope) + Art. 50(1)/(4) β†’ SyntheticMarkerNode VISIBLE (human disclosure / deep-fake label) + Art. 50(2) β†’ SyntheticMarkerNode METADATA (machine-readable marking) + Art. 73 β†’ IncidentReporterNode (real-time detection; conservative 48h/360h + ceiling + all three Art. 73 legal deadlines attached per record) Art. 3 β†’ MultiAgentBoundaryNode (internal vs. market-placed sub-agents) GDPR 5/17 β†’ PIIRedactionEngine + SessionMemoryNode (erasable per-subject memory) - prEN18283 β†’ BiasFilterNode (bias management) + Art. 10(2)(f)-(g) β†’ BiasFilterNode (runtime bias keyword gate; supports prEN 18283) Step 9 β†’ ComplianceManifestGenerator (action inventory + adjacent legislation) AEPD PoP β†’ LethalTrifectaGuard (Rule-of-2 runtime block) Fourth Tier β†’ AuthorityLedger (stakeholder/role/rationale/risk signed record) - Art. 5 β†’ ProhibitedPracticeGuard (auto-wired into executor) + Art. 5 β†’ ProhibitedPracticeGuard (auto-wired into executor; incl. the + 2 Dec 2026 Omnibus NCII/CSAM additions) BranchTriageβ†’ BranchTriageNode (fractal agents β€” see 23_fractal_compliance_showcase.py) + +Timeline (post-Omnibus): Art. 5 prohibitions in force since 2 Feb 2025 (NCII/CSAM +from 2 Dec 2026); GPAI duties since 2 Aug 2025; Art. 50 transparency from +2 Aug 2026 (2 Dec 2026 grace for the 50(2) marking of systems already on market); +Annex III high-risk obligations apply from **2 December 2027** (deferred from +2 Aug 2026); Annex I embedded high-risk from 2 Aug 2028. """ from __future__ import annotations @@ -50,6 +70,7 @@ ProhibitedPracticeGuard, # v2.2.0 gap-closure FundamentalRightsImpactNode, + Article27FRIANode, SessionMemoryNode, SupplierAgreementRegistry, DeployerTransparencyNode, @@ -84,7 +105,8 @@ "api_credential_val": os.getenv("ENTERPRISE_API_KEY", "mock-jit-token-xyz"), # ── LLM ─────────────────────────────────────────────────────────────────── - "model": "ollama/phi4:latest", + # Override for CI / machines without phi4: export LAR_SHOWCASE_MODEL=... + "model": os.getenv("LAR_SHOWCASE_MODEL", "ollama/phi4:latest"), # ── Prompt ──────────────────────────────────────────────────────────────── # {case_summary} is injected from state at runtime @@ -108,6 +130,14 @@ # ── Regulatory tags (shown in manifest + ledger) ────────────────────────── "regulatory_tags": ["EU_AI_ACT", "GDPR"], + # ── Art. 27 FRIA (deployer obligation) ──────────────────────────────────── + # deployer_class ∈ PUBLIC_BODY | PRIVATE_PUBLIC_SERVICE | CREDIT_SCORING | + # LIFE_HEALTH_INSURANCE β†’ Art. 27 FRIA applies. + # Anything else (default) β†’ not applicable; the node records the rationale. + "deployer_class": "OUT_OF_SCOPE", + "annex_iii_point": "", + "fria_dpia_reference": None, # Art. 27(4) β€” cross-refer a GDPR Art. 35 DPIA + # ── Output dir ──────────────────────────────────────────────────────────── "output_dir": "enterprise_audit", "hmac_secret": os.getenv("HMAC_SECRET", "change-me-in-prod"), @@ -124,6 +154,11 @@ "conformity_id": "CA-FIN-2026", "stakeholder_role": "Risk Officer", "regulatory_tags": ["EU_AI_ACT", "GDPR", "MIFID_II", "DORA", "FINRA"], + # Creditworthiness evaluation = Annex III 5(b) β†’ the deployer owes an + # Art. 27 Fundamental Rights Impact Assessment. + "deployer_class": "CREDIT_SCORING", + "annex_iii_point": "5(b) β€” evaluation of creditworthiness / credit scoring", + "fria_dpia_reference": "DPIA-FIN-2026-014 (GDPR Art. 35) β€” cross-referenced per Art. 27(4)", "pii_keys": ["account_number", "ssn", "iban", "email", "name", "dob"], "bias_terms": ["race", "gender", "age", "postal_code", "nationality"], "analysis_prompt": ( @@ -214,22 +249,28 @@ def build_and_run( domain: str = "GENERIC", config_overrides: Optional[Dict[str, Any]] = None, _mock_inputs: Optional[List[str]] = None, + human_decision_provider: Optional[Any] = None, ) -> Dict[str, Any]: """ - Build the full 23-requirement paper-mapped compliance graph and execute it for - a single case. Every v2.2.0 node is wired into the live execution path β€” no - requirement is merely declared; every one fires at runtime. + Build the full paper-mapped compliance graph and execute it for a single case. + Every gap-closure node is wired into the live execution path β€” no requirement + is merely declared; every one fires at runtime. Args: case: The intake payload (dict). Must contain 'case_summary'. domain: One of FINANCE | HEALTHCARE | PHARMA | LEGAL | HR | GENERIC. config_overrides: Any key from DEFAULT_CONFIG to override at call time. - _mock_inputs: For automated testing β€” replaces builtins.input responses. + _mock_inputs: DEMO ONLY β€” a flat list ``[decision, rationale, ...]`` + used to build a stand-in ``human_decision_provider`` so + the ``always_human`` jury can run without a TTY. A real + deployment passes ``human_decision_provider`` wired to a + reviewer UI / web form / Slack action instead. With + neither, the jury HALTS by design. + human_decision_provider: callable ``fn(context) -> (decision, rationale)``. Returns: - dict with keys: run_id, domain, decision, confidence, risk_level, - audit_log_path, authority_ledger_path, manifest_path, - final_state (all state keys after execution). + dict with keys: domain, system_name, audit_log_path, authority_ledger_path, + manifest_path, fria_art27_path, final_state, ... """ # ── 0. Config ───────────────────────────────────────────────────────────── cfg = build_config(domain) @@ -238,8 +279,22 @@ def build_and_run( os.makedirs(cfg["output_dir"], exist_ok=True) - # ── 0a. Mock input for non-interactive / CI runs ─────────────────────────── - if _mock_inputs: + # ── 0a. Resolve the human-decision source ───────────────────────────────── + _orig_input = None + if human_decision_provider is None and _mock_inputs: + # DEMO: turn [decision, rationale, decision, rationale, ...] into a provider + # that travels the SAME validated + ledger-recorded path as a real reviewer. + _queue = list(_mock_inputs) + + def _demo_provider(context): + dec = _queue.pop(0) if _queue else "approve" + rat = _queue.pop(0) if _queue else "Simulated reviewer rationale (demo mode)." + return dec, rat + + human_decision_provider = _demo_provider + cfg["_demo_human_decision"] = True + + # Keep the legacy builtins.input patch too, for any other input() call. _idx = [0] _orig_input = builtins.input def _fake_input(prompt=""): @@ -249,21 +304,31 @@ def _fake_input(prompt=""): return val builtins.input = _fake_input + cfg["_human_decision_provider"] = human_decision_provider + try: return _run(case, cfg) finally: - if _mock_inputs: + if _orig_input is not None: builtins.input = _orig_input def _run(case: Dict[str, Any], cfg: Dict[str, Any]) -> Dict[str, Any]: print(f"\n{'='*65}") - print(f" LΓ‘r Enterprise Compliance Backbone (v2.2.0 β€” 23 requirements)") + print(f" LΓ‘r Enterprise Compliance Backbone") print(f" Domain : {cfg['domain']}") print(f" System : {cfg['system_name']}") print(f"{'='*65}\n") - - # ── STEP 7: Credential Vault (Art. 15(4) β€” NHI privilege + trust gating) ─ + if cfg.get("_demo_human_decision"): + print(" " + "!"*61) + print(" DEMO MODE β€” the HumanJuryNode decision below is SIMULATED via a") + print(" stand-in human_decision_provider (placeholder for a real reviewer") + print(" UI / web form / Slack action). automation_boundary is 'always_human':") + print(" with no provider and no TTY this run HALTS by design. Do not read") + print(" the AuthorityLedger record from this run as real human oversight.") + print(" " + "!"*61 + "\n") + + # ── STEP 7: Credential Vault (Art. 15(5) β€” NHI least privilege + trust gating) ─ vault = CredentialVault() # Row L: register with HIGH minimum trust level β€” get_with_trust() enforces this vault.register_credential( @@ -357,7 +422,7 @@ def _run(case: Dict[str, Any], cfg: Dict[str, Any]) -> Dict[str, Any]: }, ) - # ── Row B: Behavioral Envelope Monitor (Art. 9 PMM) ───────────────────── + # ── Row B: Behavioral Envelope Monitor (Art. 9(9) post-market monitoring) ─ # Baseline confidence samples from domain conformity assessment runs envelope_monitor = BehavioralEnvelopeMonitor( metric_key="model_confidence", @@ -407,6 +472,55 @@ def _run(case: Dict[str, Any], cfg: Dict[str, Any]) -> Dict[str, Any]: next_node=None, ) + # ── Row M: Article 27 FRIA (deployer Fundamental Rights Impact Assessment) ─ + # Gated on deployer_class. FINANCE (credit scoring, Annex III 5(b)) is in scope. + node_fria27 = Article27FRIANode( + system_name=cfg["system_name"], + deployer_class=cfg.get("deployer_class", "OUT_OF_SCOPE"), + intended_purpose=( + f"High-risk AI decision support for the {cfg['domain']} domain." + ), + annex_iii_point=cfg.get("annex_iii_point", ""), + deployment_process=( + f"The {cfg['stakeholder_role']} runs each intake case through this " + f"agent for a recommendation, then reviews and approves/rejects it " + f"via the HumanJuryNode before any external write. The AI output is " + f"advisory; the human decision is determinative." + ), + usage_period_and_frequency=( + "Continuous during business hours for the lifetime of the deployment; " + "one assessment per intake case (est. 50–200/day). Reviewed quarterly." + ), + affected_natural_persons=[ + "Applicants / data subjects who are the subject of the case", + "Their dependants and guarantors where financially linked", + "Members of groups statistically over-represented in adverse outcomes", + ], + fundamental_rights_risks=[ + "Non-discrimination (Charter Art. 21) β€” proxy bias in the recommendation", + "Private life / data protection (Charter Arts. 7-8) β€” processing of PII", + "Effective remedy (Charter Art. 47) β€” opacity of an adverse recommendation", + "Consumer protection (Charter Art. 38) β€” over-reliance on an automated score", + ], + human_oversight_measures=[ + f"Mandatory {cfg['stakeholder_role']} approval gate (HumanJuryNode) before any external action", + "Reviewer sees the model's recommendation, confidence, and rationale, plus bias-scan and FRIA-screen results", + "AuthorityLedger records who decided, their role, rationale, risk score and timestamp", + "RiskScorerNode forces PRE_EXECUTION oversight for irreversible / third-party-affecting actions", + ], + measures_on_materialisation=[ + "Suspend the deployment and inform the provider and market surveillance authority (Art. 26(5), Art. 73)", + "Route affected cases to fully manual review until remediated", + "Complaint mechanism: affected persons may contest an adverse outcome and obtain a human re-review", + "Internal governance: compliance owner + quarterly FRIA review + incident log triage", + ], + provider_info_reference="Art. 13 instructions-for-use (see state['deployer_instructions'])", + dpia_reference=cfg.get("fria_dpia_reference"), + output_key="fria_art27", + strict=False, + next_node=None, + ) + # ── Row I: Multi-Agent Boundary Node (Art. 3 sub-agent classification) ─── node_boundary = MultiAgentBoundaryNode( agent_name=cfg["system_name"], @@ -421,7 +535,7 @@ def _run(case: Dict[str, Any], cfg: Dict[str, Any]) -> Dict[str, Any]: next_node=None, ) - # ── Node A: NHI credential fetch (Art. 15(4) β€” Row L: get_with_trust) ──── + # ── Node A: NHI credential fetch (Art. 15(5) β€” Row L: get_with_trust) ──── def fetch_credentials(state: GraphState): # Row G: assert supplier agreement before calling the LLM gateway supplier_registry.assert_agreement("llm_gateway") @@ -453,8 +567,9 @@ def fetch_credentials(state: GraphState): "description": "LLM inference on applicant PII data β€” directly affects the case subject's outcome.", } - # ── Row A: Fundamental Rights Impact Assessment (Art. 9 FRIA) ──────────── - # Scans LLM output for EU Charter dimension violations before human sees it + # ── Row A: Fundamental-rights screening (Art. 9(2)(a) risk factor) ─────── + # Runtime heuristic scan of the LLM output for EU Charter red flags before a + # human sees it. This is NOT the Art. 27 FRIA (that is node_fria27, above). node_fria = FundamentalRightsImpactNode( input_key="ai_output", next_node=None, @@ -503,14 +618,18 @@ def parse_output(state: GraphState): stakeholder_role=cfg["stakeholder_role"], action_description=f"{cfg['domain']} AI case analysis β€” external action pending", risk_score_key="model_confidence", - # Row F: per-decision-type automation boundary enforcement. - # Showcase uses "auto_first_choice" so the mock input path works in CI. - # Production deployments should set "always_human" to block non-interactive runs. + # Row F: per-decision-type automation boundary. Default is 'always_human' + # for the high-risk case decision: with no human_decision_provider and no + # TTY the jury HALTS (RuntimeError) by design β€” it never rubber-stamps. + # The showcase supplies a *simulated* reviewer via human_decision_provider + # (see build_and_run's _mock_inputs / human_decision_provider args), which + # travels the same validated + AuthorityLedger-recorded path as a real one. decision_type="case_analysis", automation_boundary={ - "case_analysis": "auto_first_choice", # CI/showcase mode + "case_analysis": "always_human", "output_review": "auto_if_low_risk", }, + human_decision_provider=cfg.get("_human_decision_provider"), ) # ── Node E: RiskScorerNode (Art. 14 β€” routes to jury or proceeds) ──────── @@ -521,7 +640,7 @@ def parse_output(state: GraphState): action_type_key="action_type", ) - # ── Node F: Bias filter (prEN 18283) ────────────────────────────────────── + # ── Node F: Bias filter (Art. 10(2)(f)-(g) β€” runtime keyword gate) ─────── node_bias = BiasFilterNode( input_key="recommendation", sensitive_terms=cfg["bias_terms"], @@ -595,7 +714,7 @@ def compliance_checks(state: GraphState): block_on_violation=True, ) - # ── Node H: Synthetic marker (Art. 50(2)) ───────────────────────────────── + # ── Node H: Synthetic marker β€” Art. 50(1)/(4) human disclosure ─────────── node_marker = SyntheticMarkerNode( input_key="recommendation", output_key="final_output", @@ -603,6 +722,14 @@ def compliance_checks(state: GraphState): next_node=None, ) + # ── Node H2: Synthetic marker β€” Art. 50(2) machine-readable marking ────── + node_marker_meta = SyntheticMarkerNode( + input_key="final_output", + output_key="final_output_marked", + marker_type="METADATA", + next_node=None, + ) + # ── Row K: Session Memory β€” erase (GDPR Art. 17 right to erasure) ───────── # Erase demonstrates that per-subject data can be deleted on request node_session_erase = SessionMemoryNode( @@ -614,15 +741,16 @@ def compliance_checks(state: GraphState): # ── Wire the graph ──────────────────────────────────────────────────────── # - # [deployer] β†’ [boundary] β†’ [creds] β†’ [llm] β†’ [fria] β†’ [parse] + # [deployer] β†’ [fria27] β†’ [boundary] β†’ [creds] β†’ [llm] β†’ [fria] β†’ [parse] # β†’ [session_write] β†’ [bias] β†’ [risk] β†’ [jury] β†’ [tool_monitor] - # β†’ [checks] β†’ [prohibited] β†’ [marker] β†’ [session_erase] + # β†’ [checks] β†’ [prohibited] β†’ [marker] β†’ [marker_meta] β†’ [session_erase] # # BiasFilterNode: normal β†’ [risk]; bias detected β†’ [jury] # RiskScorerNode: PRE_EXECUTION β†’ [jury]; LOW/MEDIUM β†’ [tool_monitor] # HumanJuryNode: approved β†’ [tool_monitor] # - node_deployer.next_node = node_boundary + node_deployer.next_node = node_fria27 + node_fria27.next_node = node_boundary node_boundary.next_node = node_creds node_creds.next_node = node_llm node_llm.next_node = node_fria @@ -635,7 +763,8 @@ def compliance_checks(state: GraphState): node_tool_monitor.next_node = node_checks node_checks.next_node = node_prohibited node_prohibited.next_node = node_marker - node_marker.next_node = node_session_erase + node_marker.next_node = node_marker_meta + node_marker_meta.next_node = node_session_erase # ── STEP 10: ComplianceManifestGenerator (Step 9 β€” action inventory) ────── manifest = ComplianceManifestGenerator( @@ -694,14 +823,34 @@ def compliance_checks(state: GraphState): # Capture final state from last executed step final_state_dict = all_steps[-1].get("state_after", {}) if all_steps else {} + # ── Art. 27 FRIA β€” persist the deployer artifact (Markdown + JSON) ──────── + fria_art27_path = None + fria_record = final_state_dict.get("fria_art27") + if fria_record is not None: + fria_art27_path = f"{cfg['output_dir']}/fria_art27.md" + try: + # Rebuild a lightweight state shim for as_markdown() + _shim = GraphState() + _shim.set("fria_art27", fria_record) + with open(fria_art27_path, "w") as _f: + _f.write(node_fria27.as_markdown(_shim)) + with open(f"{cfg['output_dir']}/fria_art27.json", "w") as _f: + json.dump(fria_record, _f, indent=2) + print(f" [Article27FRIANode]: FRIA artifact saved to {fria_art27_path}") + except Exception as _e: # pragma: no cover - artifact write is best-effort + print(f" [Article27FRIANode]: could not write FRIA artifact: {_e}") + return { "domain": cfg["domain"], "system_name": cfg["system_name"], "audit_log_path": audit_log_path, "authority_ledger_path": ledger_path, "manifest_path": manifest_path, + "fria_art27_path": fria_art27_path, + "fria_art27": fria_record, "authority_records": authority_ledger.get_records(), - # v2.2.0 runtime verification keys + "demo_human_decision": bool(cfg.get("_demo_human_decision")), + # runtime verification keys "final_state": final_state_dict, "integrity_results": integrity_results, "incident_log_path": incident_reporter.incident_log_path, diff --git a/src/lar/enterprise/run.py b/src/lar/enterprise/run.py index 9a991ef..8dbf65c 100644 --- a/src/lar/enterprise/run.py +++ b/src/lar/enterprise/run.py @@ -1,23 +1,32 @@ """ Enterprise Compliance Reference Implementation =============================================== -Run this file to see the full 12-primitive LΓ‘r compliance backbone working -end-to-end on a realistic case. +Run this file to see the full LΓ‘r compliance backbone working end-to-end on a +realistic case. Article citations are against Regulation (EU) 2024/1689 as +amended by the Digital Omnibus (Regulation (EU) 2026/1744). Usage: python run.py # defaults to HEALTHCARE demo - python run.py FINANCE # finance credit-risk case + python run.py FINANCE # finance credit-risk case (Art. 27 FRIA applies) python run.py PHARMA # clinical-trial eligibility python run.py LEGAL # legal triage python run.py HR # recruitment screening + LAR_SHOWCASE_MODEL=ollama/llama3.2:1b python run.py FINANCE # override the LLM + To target your own domain, add an entry to backbone.DOMAIN_PRESETS or pass config_overrides= to build_and_run(). +DEMO NOTE: the HumanJuryNode below is configured automation_boundary="always_human". +The `mock` list is turned into a *simulated* reviewer (human_decision_provider); +with no provider and no TTY the run HALTS by design. Production wires a real +reviewer surface. + All output is written to ./enterprise_audit/ run_.json β†’ HMAC-signed causal audit log (Art. 12) authority_ledger.json β†’ HMAC-signed authority records (Art. 12/14) - compliance_manifest.json β†’ Regulatory action inventory (Step 9) + compliance_manifest.json β†’ Regulatory action inventory (Step 9 / Annex IV Β§2) + fria_art27.md β†’ deployer Fundamental Rights Impact Assessment (Art. 27; in-scope domains) """ import sys @@ -92,8 +101,9 @@ def main(): case = CASES[domain] - # Human jury input simulation (approve + rationale). - # Remove _mock_inputs to run interactively in production. + # Simulated reviewer decision + rationale (becomes a human_decision_provider). + # Remove _mock_inputs to run interactively; in production pass a real + # human_decision_provider wired to your reviewer surface. mock = ["approve", f"Reviewed {domain} case. AI recommendation verified against policy."] result = build_and_run( @@ -108,6 +118,14 @@ def main(): print(f" Audit log : {result['audit_log_path']}") print(f" Authority ledger: {result['authority_ledger_path']}") print(f" Manifest : {result['manifest_path']}") + fria = result.get("fria_art27") or {} + if fria.get("applicable"): + print(f" Art. 27 FRIA : {result.get('fria_art27_path')} " + f"(complete={fria.get('completeness', {}).get('complete')})") + else: + print(f" Art. 27 FRIA : not applicable for deployer_class=" + f"{fria.get('deployer_class', 'OUT_OF_SCOPE')} " + f"(rationale recorded at {result.get('fria_art27_path')})") records = result["authority_records"] if records: r = records[0] @@ -117,11 +135,11 @@ def main(): print(f" Rationale : {r['rationale']}") print(f" Risk score : {r['risk_score_at_decision']}") print(f" Timestamp : {r['timestamp']}") - print(f"\n βœ… All 13 compliance primitives executed.") - print(f" βœ… Audit artefacts are HMAC-SHA256 signed.") - print(f" βœ… PII stripped from logs before signing.") - print(f" βœ… Lethal trifecta check passed (jury prior approval on record).") - print(f" βœ… Regulatory action inventory saved to manifest.") + print(f"\n OK Compliance backbone executed end-to-end" + f"{' (DEMO reviewer)' if result.get('demo_human_decision') else ''}.") + print(f" OK Audit artefacts are HMAC-SHA256 signed; PII stripped before signing.") + print(f" OK Lethal trifecta check passed (jury prior approval on record).") + print(f" OK Regulatory action inventory saved to manifest.") print() diff --git a/src/lar/node.py b/src/lar/node.py index b36aa4f..0f988c9 100644 --- a/src/lar/node.py +++ b/src/lar/node.py @@ -834,6 +834,8 @@ def __init__(self, # --- Art. 14 Automation Boundary (v2.2.0) --- decision_type: Optional[str] = None, automation_boundary: Optional[Dict[str, str]] = None, + # --- Out-of-band real human decision (v2.3.1) --- + human_decision_provider: Optional[Any] = None, # --- Durable pause/resume (v2.3.0) --- checkpoint_store: Optional[Any] = None, case_id_key: Optional[str] = None): @@ -866,6 +868,17 @@ def __init__(self, "CREDIT_DECISION": "always_human", "ALERT_REVIEW": "auto_if_low_risk", } + human_decision_provider: Optional callable ``fn(context: dict) -> + (decision: str, rationale: str)``. When set, the decision is + sourced from it and travels the SAME path as an interactive + decision β€” validated against ``choices``, rationale required, + AuthorityRecord written β€” but WITHOUT needing a TTY. This models + a real human answering out-of-band (web form, Slack action, API, + ``lar.checkpoint.resume_human_decision``). It is NOT an + automation-boundary fallback: with ``automation_boundary`` set to + ``"always_human"`` and NO provider and no TTY, the node still + halts by design. ``context`` is ``{k: state.get(k) for k in + context_keys}`` plus ``prompt``/``choices``/``decision_type``. checkpoint_store: If provided, the pause is written durably (via ``checkpoint_store.save()``) BEFORE this node blocks on ``input()`` or applies an automation_boundary policy β€” so the @@ -909,6 +922,10 @@ def __init__(self, # Automation boundary (Art. 14) self.decision_type = decision_type self.automation_boundary: Dict[str, str] = automation_boundary or {} + # Out-of-band real human decision source (not an automation fallback) + if human_decision_provider is not None and not callable(human_decision_provider): + raise ValueError("human_decision_provider must be callable") + self.human_decision_provider = human_decision_provider # Durable pause/resume (v2.3.0) if checkpoint_store is not None and not case_id_key: raise ValueError("case_id_key is required when checkpoint_store is provided") @@ -966,9 +983,36 @@ def execute(self, state: GraphState): print(f" [HumanJuryNode]: Checkpoint saved for case_id='{case_id}' β€” " f"this pause now survives a process restart.") - # 2. Loop until valid input β€” with automation_boundary policy for non-interactive envs + # 2. Resolve the decision. rationale = "" - if not sys.stdin.isatty(): + + # 2a. Out-of-band real human decision (web form / Slack / API / resume). + # Travels the same validation + ledger path as an interactive decision, + # without needing a TTY. This is NOT an automation fallback. + if self.human_decision_provider is not None: + ctx = {k: state.get(k) for k in self.context_keys} + ctx.update({ + "prompt": self.prompt, + "choices": list(self.choices), + "decision_type": self.decision_type, + }) + provided = self.human_decision_provider(ctx) + if isinstance(provided, (tuple, list)) and len(provided) == 2: + user_input, rationale = str(provided[0]).strip().lower(), str(provided[1]).strip() + else: + user_input, rationale = str(provided).strip().lower(), "" + if user_input not in self.choices: + raise ValueError( + f"[HumanJuryNode] human_decision_provider returned '{user_input}', " + f"not one of {self.choices}" + ) + if self.authority_ledger and not rationale: + rationale = f"No rationale provided. Decision: {user_input}." + print(f" [HumanJuryNode]: Out-of-band human decision '{user_input}' recorded.") + state.set(self.output_key, user_input) + + # 2b. Interactive / automation-boundary path. + elif not sys.stdin.isatty(): # Determine fallback policy for this decision_type policy = self.automation_boundary.get( self.decision_type or "", "auto_first_choice" diff --git a/tests/unit/test_compliance_conformance_fixes.py b/tests/unit/test_compliance_conformance_fixes.py new file mode 100644 index 0000000..208386c --- /dev/null +++ b/tests/unit/test_compliance_conformance_fixes.py @@ -0,0 +1,301 @@ +""" +Regression tests for the 2026-09 EU AI Act conformance pass: + +- Art. 15(4) -> 15(5) citation correction (cybersecurity is 15(5) in the enacted + Regulation (EU) 2024/1689; 15(4) is robustness / feedback loops). +- Article27FRIANode: the deployer Art. 27 Fundamental Rights Impact Assessment + (scope gating, completeness, strict mode, markdown). +- HumanJuryNode.human_decision_provider: a real out-of-band human decision that + bypasses the TTY gate but keeps the validated + ledger-recorded path. +- ProhibitedPracticeGuard: 2 Dec 2026 Omnibus NCII / CSAM additions. +- SyntheticMarkerNode: Art. 50(2) (machine-readable) vs 50(1)/(4) (visible). +- IncidentReporterNode: conservative deadline ceiling + all three Art. 73 + legal deadlines attached, no false "death" classification. +""" + +import json +import pytest + +from lar import HumanJuryNode +from lar.state import GraphState +from lar.compliance import ( + CredentialVault, + Article27FRIANode, + Art27FRIAIncompleteError, + ProhibitedPracticeGuard, + ProhibitedPracticeError, + SyntheticMarkerNode, + PromptInjectionGuard, + ComplianceManifestGenerator, +) +from lar.compliance.incident_reporter import IncidentReporterNode + + +def _gs(d=None): + return GraphState(d or {}) + + +# ───────────────────────────────────────────────────────────────────────────── +# Art. 15(5) citation correction +# ───────────────────────────────────────────────────────────────────────────── + +class TestArticle155Citation: + + def test_credential_vault_reference_is_15_5(self): + assert "15(5)" in CredentialVault.EU_REFERENCE + assert "15(4)" not in CredentialVault.EU_REFERENCE + + def test_prompt_injection_guard_cites_15_5(self): + # PromptInjectionGuard was already correct; lock it in. + assert "15(5)" in (PromptInjectionGuard.__doc__ or "") + + def test_manifest_toolnode_article_is_15_5(self): + class _FakeToolFn: + __name__ = "send_email" + __module__ = "x" + + class _FakeTool: + def __init__(self): + self.tool_function = _FakeToolFn() + self.action_type = "SEND_EMAIL" + self.affected_parties = "THIRD_PARTY" + self.input_keys = ["body"] + self.output_key = "status" + self.credential_vault = None + self.credential_key = None + self.next_node = None + self.error_node = None + + _FakeTool.__name__ = "ToolNode" + gen = ComplianceManifestGenerator(start_node=_FakeTool(), system_name="t") + m = gen.generate() + blob = json.dumps(m) + assert "15(5)" in blob + assert "15(4)" not in blob + + +# ───────────────────────────────────────────────────────────────────────────── +# Article27FRIANode +# ───────────────────────────────────────────────────────────────────────────── + +class TestArticle27FRIANode: + + def _complete_kwargs(self): + return dict( + deployment_process="Officer runs each case, then approves via jury.", + usage_period_and_frequency="Continuous; ~100/day; reviewed quarterly.", + affected_natural_persons=["Applicants", "Guarantors"], + fundamental_rights_risks=["Non-discrimination (Art. 21)"], + human_oversight_measures=["Mandatory approval gate"], + measures_on_materialisation=["Suspend + notify authority", "Complaint channel"], + ) + + def test_out_of_scope_records_not_applicable(self): + node = Article27FRIANode(system_name="X", deployer_class="OUT_OF_SCOPE") + st = _gs() + node.execute(st) + rec = st.get("fria_art27") + assert rec["applicable"] is False + assert "not in that set" in rec["reason"] or "not applicable" in rec["reason"].lower() \ + or "Annex III" in rec["reason"] + + def test_none_deployer_class_is_out_of_scope(self): + node = Article27FRIANode(system_name="X", deployer_class=None) + st = _gs() + node.execute(st) + assert st.get("fria_art27")["applicable"] is False + + def test_credit_scoring_in_scope_and_complete(self): + node = Article27FRIANode( + system_name="Credit Agent", + deployer_class="CREDIT_SCORING", + annex_iii_point="5(b)", + **self._complete_kwargs(), + ) + st = _gs() + node.execute(st) + rec = st.get("fria_art27") + assert rec["applicable"] is True + assert rec["completeness"]["complete"] is True + assert rec["completeness"]["missing_elements"] == [] + assert st.get("fria_art27_notify_authority_required") is True + assert rec["notify_market_surveillance_authority"]["required"] is True + + def test_in_scope_incomplete_lists_missing(self): + node = Article27FRIANode( + system_name="Credit Agent", + deployer_class="PUBLIC_BODY", + deployment_process="only this one filled", + ) + st = _gs() + node.execute(st) + rec = st.get("fria_art27") + assert rec["applicable"] is True + assert rec["completeness"]["complete"] is False + assert len(rec["completeness"]["missing_elements"]) == 5 + + def test_strict_mode_raises_when_incomplete(self): + node = Article27FRIANode( + system_name="X", deployer_class="LIFE_HEALTH_INSURANCE", strict=True + ) + with pytest.raises(Art27FRIAIncompleteError): + node.execute(_gs()) + + def test_markdown_render_in_scope(self): + node = Article27FRIANode( + system_name="Credit Agent", deployer_class="CREDIT_SCORING", + annex_iii_point="5(b)", dpia_reference="DPIA-1", **self._complete_kwargs(), + ) + st = _gs() + node.execute(st) + md = node.as_markdown(st) + assert "Article 27" in md + assert "## (a)" in md and "## (f)" in md + assert "DPIA-1" in md + + def test_markdown_render_out_of_scope(self): + node = Article27FRIANode(system_name="X", deployer_class="OUT_OF_SCOPE") + st = _gs() + node.execute(st) + assert "Not Applicable" in node.as_markdown(st) + + def test_next_node_is_returned(self): + sentinel = object() + node = Article27FRIANode(system_name="X", deployer_class="OUT_OF_SCOPE", + next_node=sentinel) + assert node.execute(_gs()) is sentinel + + +# ───────────────────────────────────────────────────────────────────────────── +# HumanJuryNode.human_decision_provider +# ───────────────────────────────────────────────────────────────────────────── + +class TestHumanDecisionProvider: + + def test_provider_decision_travels_validated_path(self): + seen = {} + + def provider(ctx): + seen.update(ctx) + return "approve", "looks fine" + + node = HumanJuryNode( + prompt="ok?", choices=["approve", "reject"], output_key="d", + context_keys=["risk"], human_decision_provider=provider, + ) + st = _gs({"risk": "HIGH"}) + node.execute(st) + assert st.get("d") == "approve" + assert seen["risk"] == "HIGH" + assert seen["prompt"] == "ok?" + assert seen["choices"] == ["approve", "reject"] + + def test_provider_invalid_choice_raises(self): + node = HumanJuryNode( + prompt="ok?", choices=["approve", "reject"], output_key="d", + human_decision_provider=lambda ctx: ("maybe", "?"), + ) + with pytest.raises(ValueError): + node.execute(_gs()) + + def test_provider_writes_authority_record(self): + from lar.compliance import AuthorityLedger + ledger = AuthorityLedger() + node = HumanJuryNode( + prompt="approve credit?", choices=["approve", "reject"], output_key="d", + authority_ledger=ledger, stakeholder_id="r@x.com", + stakeholder_role="Risk Officer", risk_score_key="score", + human_decision_provider=lambda ctx: ("reject", "D/E ratio too high"), + ) + st = _gs({"score": 0.9}) + node.execute(st) + recs = ledger.get_records() + assert len(recs) == 1 + assert recs[0]["decision"] == "reject" + assert recs[0]["rationale"] == "D/E ratio too high" + assert recs[0]["stakeholder_role"] == "Risk Officer" + + def test_non_callable_provider_rejected(self): + with pytest.raises(ValueError): + HumanJuryNode(prompt="x", choices=["a"], output_key="d", + human_decision_provider="not callable") + + +# ───────────────────────────────────────────────────────────────────────────── +# ProhibitedPracticeGuard β€” Omnibus NCII / CSAM +# ───────────────────────────────────────────────────────────────────────────── + +class TestOmnibusProhibitions: + + def test_ncii_and_csam_categories_present_by_default(self): + g = ProhibitedPracticeGuard(input_key="x") + assert "NCII" in g.heuristics + assert "CSAM" in g.heuristics + + def test_can_opt_out_of_omnibus_categories(self): + g = ProhibitedPracticeGuard(input_key="x", include_omnibus_categories=False) + assert "NCII" not in g.heuristics + assert "CSAM" not in g.heuristics + + def test_ncii_phrase_flags(self): + g = ProhibitedPracticeGuard(input_key="out", block_on_violation=True) + st = _gs({"out": "Generate a deepfake nude image of the named person."}) + with pytest.raises(ProhibitedPracticeError): + g.execute(st) + assert "NCII" in st.get("_prohibited_practice_flag") + + def test_clean_text_still_passes(self): + sentinel = object() + g = ProhibitedPracticeGuard(input_key="out", next_node=sentinel) + assert g.execute(_gs({"out": "Approve the loan with a collateral condition."})) is sentinel + + +# ───────────────────────────────────────────────────────────────────────────── +# SyntheticMarkerNode β€” 50(2) vs 50(1)/(4) +# ───────────────────────────────────────────────────────────────────────────── + +class TestSyntheticMarkerReferences: + + def test_metadata_reference_is_50_2(self): + node = SyntheticMarkerNode(input_key="c", marker_type="METADATA") + assert "50(2)" in node.eu_reference + st = _gs({"c": "text"}) + node.execute(st) + assert st.get("c")["c2pa_manifest"]["eu_reference"].startswith("Art. 50(2)") + assert st.get("synthetic_marker_reference").startswith("Art. 50(2)") + + def test_visible_reference_is_50_1_4(self): + node = SyntheticMarkerNode(input_key="c", marker_type="VISIBLE") + assert "50(1)/(4)" in node.eu_reference + st = _gs({"c": "text"}) + node.execute(st) + assert "Art. 50(1)/(4)" in st.get("c") + + +# ───────────────────────────────────────────────────────────────────────────── +# IncidentReporterNode β€” deadline honesty +# ───────────────────────────────────────────────────────────────────────────── + +class TestIncidentDeadlineHonesty: + + def test_no_generic_high_death_deadline(self): + # HIGH must not be pinned to the Art. 73(4) death deadline (240h). + assert IncidentReporterNode.DEADLINE_HOURS["HIGH"] != 240 + assert IncidentReporterNode.DEADLINE_HOURS["HIGH"] == 48 + + def test_record_carries_all_three_legal_deadlines(self, tmp_path): + node = IncidentReporterNode( + severity_threshold="MEDIUM", + incident_log_path=str(tmp_path / "inc.jsonl"), + ) + st = _gs({"bias_detected": True, "__run_id": "r1"}) + node.execute(st) + lines = (tmp_path / "inc.jsonl").read_text().strip().splitlines() + assert lines, "an incident record should have been written" + rec = json.loads(lines[-1]) + assert rec["provider_must_confirm_applicable_paragraph"] is True + assert rec["reporting_deadline_is_conservative_ceiling"] is True + legal = rec["art_73_legal_deadlines"] + assert "73(2)_general_default" in legal + assert "73(3)_widespread_infringement_or_serious_incident_3_49_b" in legal + assert "73(4)_death_of_a_person" in legal From 2de3b296a0612efa0c7186afb7d07ca0d8ff5911 Mon Sep 17 00:00:00 2001 From: Aadithya Vishnu Date: Wed, 9 Sep 2026 02:01:59 +0100 Subject: [PATCH 2/3] docs(changelog): add 2.4.0 EU AI Act conformance pass entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --- CHANGELOG.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a786381..89f45fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,159 @@ All notable changes to LΓ‘r are documented here. +## [2.4.0] β€” 2026-09-09 + +### EU AI Act conformance pass + +Full re-audit of `src/lar/compliance/` against **Regulation (EU) 2024/1689** as +amended by the **Digital Omnibus, Regulation (EU) 2026/1744** (in force +27 Jul 2026), cross-checked against EUR-Lex, artificialintelligenceact.eu and the +CEN-CENELEC JTC 21 work programme. The Aug-2025 citation pass fixed the code but +never reached the docs; one systemic article-number error was still live; and the +whole compliance corpus pre-dated the Omnibus deferral of the high-risk regime. + +Framing clarified everywhere: **"covered" = a runtime hook fires and/or an +evidence artifact is produced.** It is not a conformity assessment and does not by +itself discharge the obligation. LΓ‘r is a component in the Art. 25 value chain, +not the provider. + +### Added + +- **`Article27FRIANode` (`lar.compliance.fria_art27_node`) β€” the actual Article 27 + deployer Fundamental Rights Impact Assessment.** Distinct from + `FundamentalRightsImpactNode`, which is the Art. 9(2)(a) *runtime screen* and was + never the named FRIA. Scope-gated on `deployer_class` (`PUBLIC_BODY` / + `PRIVATE_PUBLIC_SERVICE` / `CREDIT_SCORING` = Annex III 5(b) / + `LIFE_HEALTH_INSURANCE` = 5(c)); anything else records an explicit + "not applicable" rationale. For an in-scope deployer it generates the + Art. 27(1)(a)–(f) template (deployer processes; period/frequency; affected + natural persons; specific fundamental-rights risks informed by the Art. 13 + provider information; human-oversight measures; measures on materialisation incl. + internal governance and complaint mechanisms), reports completeness (optional + `strict=True` raises `Art27FRIAIncompleteError` on a gap), records the + Art. 27(3) authority-notification duty, and carries an Art. 27(4) DPIA + cross-reference. `as_markdown()` produces the filing-ready document. Exported + from `lar.compliance` alongside `Art27FRIAIncompleteError`. +- **`HumanJuryNode(human_decision_provider=…)`** β€” optional callable + `fn(context) -> (decision, rationale)` supplying a *real* human decision + out-of-band (web form, Slack action, API, `lar.checkpoint.resume_human_decision`). + It travels the same path as an interactive decision β€” validated against + `choices`, rationale required, `AuthorityRecord` written β€” but without needing a + TTY. It is **not** an automation-boundary fallback: with `automation_boundary` + set to `"always_human"` and no provider and no TTY, the node still halts by + design. Purely additive; every existing `HumanJuryNode` call site is unaffected. +- **`ProhibitedPracticeGuard`** gains `NCII` (non-consensual intimate imagery) and + `CSAM` (AI-generated child sexual abuse material) heuristic categories β€” the + Digital Omnibus additions to Art. 5, applicable **2 December 2026**. On by + default; `include_omnibus_categories=False` restores the pre-2026 set only. +- **`SyntheticMarkerNode`** now exposes an `eu_reference` per `marker_type` and + writes `state["synthetic_marker_reference"]`; the `METADATA` C2PA manifest + carries its Art. 50(2) basis. +- **`IncidentReporterNode`** records now carry `art_73_legal_deadlines` (all three + Art. 73 paragraphs), `reporting_deadline_is_conservative_ceiling: true` and + `provider_must_confirm_applicable_paragraph: true` (schema bumped to + `lar-incident-v2`). +- **New tests** β€” `tests/unit/test_compliance_conformance_fixes.py` (23 tests) + locking in the 15(5) correction, `Article27FRIANode` (scope / completeness / + strict / markdown), `human_decision_provider`, the NCII/CSAM categories, the + 50(2)-vs-50(1)/(4) split, and incident-deadline honesty. +- **`LAR_SHOWCASE_MODEL`** env var overrides the enterprise backbone's LLM + (default unchanged: `ollama/phi4:latest`), for CI and machines without phi4. + +### Changed + +- **BREAKING (enterprise backbone only): the compliance backbone's `HumanJuryNode` + now defaults to `automation_boundary={"case_analysis": "always_human"}`** β€” it + previously used `"auto_first_choice"`, which auto-approved the high-risk case + decision in non-interactive runs and still wrote an `AuthorityLedger` record + with no reachable human. It now **halts** (`RuntimeError`) with no + `human_decision_provider` and no TTY. `build_and_run(..., _mock_inputs=[…])` + and the finance showcase supply a *simulated* reviewer via + `human_decision_provider` (with a loud DEMO banner) so demo runs still complete + without demonstrating rubber-stamping. Core `HumanJuryNode` defaults are + unchanged; this only affects `lar.enterprise.backbone`. +- **Cybersecurity citations corrected from Art. 15(4) to Art. 15(5)** across + `credential_vault.py` (`EU_REFERENCE` + docstrings + the `PermissionError` + message), `manifest.py` (ToolNode / generic-node article + the unvaulted-tools + risk flag), `backbone.py`, and ~10 documentation files. In Regulation (EU) + 2024/1689 as enacted, **15(5)** is cybersecurity (resilience against + unauthorised third parties, data/model poisoning, adversarial examples) and + 15(4) is robustness / error-resilience / feedback loops. `prompt_injection_guard.py` + was already correct. `BatchNode` state-isolation in `manifest.py` deliberately + **stays at 15(4)** β€” that is a robustness measure. NHI least privilege is now + framed as a *proportionate supporting measure* under 15(5) (aligned with + prEN 18282), not as verbatim Article text. +- **`SyntheticMarkerNode` citations split**: `METADATA` β†’ Art. 50(2) + (machine-readable marking, providers of generative systems); `VISIBLE` β†’ + Art. 50(1)/(4) (human-readable disclosure to affected persons / deep-fake & + public-interest-text labelling). Previously the visible path was filed under + 50(2). The `c2pa_manifest.generator` string now reads `lar.__version__` instead + of a hardcoded `"Lar Framework v1.5.1"`. +- **`IncidentReporterNode.DEADLINE_HOURS`**: `HIGH` moved from 240 h (the + Art. 73(4) "death of a person" deadline) to 48 h. Mapping an unclassified + severity tier onto the death paragraph was a false legal classification. Values + are now a conservative ceiling only (CRITICAL 48 h, HIGH 48 h, MEDIUM 360 h, + LOW none); the real per-paragraph deadlines ride on every record and the + provider must confirm the applicable paragraph. +- **`FundamentalRightsImpactNode`** re-labelled throughout the docs and the + backbone as the **Art. 9(2)(a) runtime fundamental-rights screen**, with an + explicit "this is NOT the Article 27 FRIA" cross-reference to `Article27FRIANode`. + (The node's own docstring was already corrected in 2.2.2; the docs had not + caught up and still said "Art. 9 FRIA".) +- **`BiasFilterNode`** docstring and the manifest entry re-scoped: it is a + *runtime bias keyword gate* that **supports**, but does not by itself satisfy, + prEN 18283 or the Art. 10(2)(f)-(g) examination for bias. +- **`ComplianceManifestGenerator`** reference string: "Step 9 β€” External Action + Inventory" β†’ "…feeds Annex IV Β§2(b) (components and their interactions); the + full Annex IV technical documentation remains a provider deliverable". The + `BatchNode` inventory entry now cites Art. 15(4) (robustness β€” state isolation) + and `prEN 18229-2` instead of a bare `prEN 18229`. +- **`IncidentReporter`** (the PMM report generator) docstring: it **supports** + Art. 72 by aggregating operational data; it does not replace the documented + post-market monitoring plan (Art. 72(3)). +- **`SupplierAgreementRegistry`** docstring: added the Art. 25(4) **free/open-source + carve-out** β€” LΓ‘r itself (Apache-2.0, not a GPAI model) is outside Art. 25(4) as + a supplied component; the registry is for the customer-provider's *other* + non-FOSS suppliers. +- **Documentation & README** updated for the **Digital Omnibus**: standalone + Annex III high-risk obligations now apply from **2 December 2027** (deferred + from 2 Aug 2026), Annex I embedded high-risk from 2 Aug 2028; Art. 5, GPAI and + Art. 50 are unaffected (2 Dec 2026 grace for the 50(2) marking of systems + already on the market). README badge "EU AI Act β€” Ready Aug 2026" β†’ + "evidence infrastructure"; the "First EU AI Act-Ready" tagline and inflated + primitive counts removed; new "Digital Omnibus" sections in + `docs/compliance/paper-compliance-mapping.md` and + `docs/compliance/eu-ai-act-deep-dive.md`; new Artefact 4 (`fria_art27.md`), + red-flag rows and reviewer questions in `docs/compliance/auditor_guide.md`. +- **`examples/compliance/22_eu_ai_act_finance_showcase.py`** rewritten: 26 + verification rows (added M = Art. 27 FRIA, N = NCII/CSAM, O = both Art. 50 + markings), corrected citations, a Digital Omnibus timeline panel, a DEMO-mode + explanation, and the `LAR_SHOWCASE_MODEL` override. The backbone now also runs a + second `SyntheticMarkerNode` (`METADATA`) so both Art. 50 markings are exercised. + +### Fixed + +- Stale documentation that contradicted the 2.2.2 code fixes: "Art. 9 FRIA" + (now Art. 9(2)(a) screen vs Art. 27 FRIA), invented "24h / 72h" incident + deadlines (now the real 48 h / 240 h / 360 h, keyed to incident type), and a + "Recital 12 β€” multi-actor chains" reference in `docs/compliance.md` + (Recital 12 is about the definition of "AI system"). +- `docs/compliance/finance-showcase.md` referenced a non-existent `BACKBONE_MODEL` + env var; replaced with the real `LAR_SHOWCASE_MODEL`. + +### Verified + +- `python examples/compliance/22_eu_ai_act_finance_showcase.py` β€” **24 / 24 + runtime checks pass** (against a local model). +- Test suite green, including the 23 new conformance tests. + +### Note + +pyproject `version` and the release tag are intentionally left for the maintainer +to bump β€” this branch layers on top of unreleased 2.3.x checkpoint work. + +--- + ## [2.3.0] β€” 2026-08-18 ### Added From a7e95ee65fe370f9727077c237281a518b611a6e Mon Sep 17 00:00:00 2001 From: Aadithya Vishnu Date: Wed, 9 Sep 2026 02:10:27 +0100 Subject: [PATCH 3/3] docs: align compliance-primitive count to 22 (adds Article27FRIANode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches snath-website. Was a vague "~24" / stale "20" across README and docs/index.md. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --- README.md | 6 +++--- docs/index.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ec8aeca..950131e 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ LΓ‘r is the execution spine of a three-part system. Each repo is independent and | Repository | Role | | :--- | :--- | -| **[LΓ‘r](https://github.com/snath-ai/lar)** ← you are here | The execution spine β€” deterministic graph engine, ~24 EU AI Act compliance primitives, HMAC audit trail | +| **[LΓ‘r](https://github.com/snath-ai/lar)** ← you are here | The execution spine β€” deterministic graph engine, 22 EU AI Act compliance primitives, HMAC audit trail | | **[LΓ‘r-JEPA](https://github.com/snath-ai/Lar-JEPA)** | The world model β€” 10 ABCs spanning the full inference-time contract (divergence routing, modal encoding, fault localisation, adapter routing) | | **[LΓ‘r DMN](https://github.com/snath-ai/DMN)** | The memory blueprint β€” `AbstractDMN` + `AbstractAdapterRouter`; domain implementations own storage and LoRA fitting; zero runtime dependencies | @@ -108,7 +108,7 @@ Every step produces a real Article 12 causal trace, a real Article 14 AuthorityL **[Full showcase breakdown β€” execution trace, real JSON artefacts, 12-step coverage map β†’](https://docs.snath.ai/compliance/finance-showcase/)** -> **Open-source vs. enterprise:** All ~24 compliance primitives (`FundamentalRightsImpactNode`, `IncidentReporterNode`, `BehavioralEnvelopeMonitor`, `SessionMemoryNode`, `BranchTriageNode`, `CredentialVault`, etc.) are in `lar.compliance` and fully open-source under Apache 2.0. The finance showcase uses `build_and_run` from `lar.enterprise`, a convenience wrapper in the enterprise tier. Open-source users assemble the same pipeline from primitives β€” the step-by-step guide walks you through it: **[Build a Compliant Agent from Scratch β†’](https://docs.snath.ai/guides/build-compliant-agent/)** +> **Open-source vs. enterprise:** All 22 compliance primitives (`FundamentalRightsImpactNode`, `IncidentReporterNode`, `BehavioralEnvelopeMonitor`, `SessionMemoryNode`, `BranchTriageNode`, `CredentialVault`, etc.) are in `lar.compliance` and fully open-source under Apache 2.0. The finance showcase uses `build_and_run` from `lar.enterprise`, a convenience wrapper in the enterprise tier. Open-source users assemble the same pipeline from primitives β€” the step-by-step guide walks you through it: **[Build a Compliant Agent from Scratch β†’](https://docs.snath.ai/guides/build-compliant-agent/)** ### The High-Stakes Loan Underwriter (Compliance-by-Design vs. LLM-as-Judge) @@ -150,7 +150,7 @@ This table was revised after an independent verification pass (this repo's own C | **Debugging** | Stack trace from inside `AgentExecutor`. | Exact node, exact error, exact state β€” in the log. | | **Auditability** | Built-in tracing exists; LangSmith is the polished, marketed observability product and has a paid tier. | Built-in. The `GraphExecutor` flight log is the audit trail β€” not an add-on, the control-flow mechanism itself. | | **Multi-Agent** | AutoGen's `GroupChat` / CrewAI's hierarchical process pick among pre-defined agents dynamically. | Deterministic assembly line β€” you define the exact path (or `AdaptiveNode` composes and *validates* new structure before running it β€” see below). | -| **Compliance** | No built-in EU AI Act primitives. | ~24 compliance primitives, cryptographic logs, Art. 14 oversight β€” mapped to Nannini et al. (2026)'s 12-step architecture. | +| **Compliance** | No built-in EU AI Act primitives. | 22 compliance primitives, cryptographic logs, Art. 14 oversight β€” mapped to Nannini et al. (2026)'s 12-step architecture. | | **Cost** | Costs an LLM call if you choose to route via a prompt (a common but not universal pattern). | Code-based routing β€” $0.00/route, by default. | | **Scale / recursion** | **Corrected**: LangGraph does NOT crash by default β€” verified directly, 25/26/100/500-step graphs all complete with no explicit config. `recursion_limit` is a real, opt-in safety feature, not a default trap. | Verified directly too: v2.2.3 fixed a real bug where LΓ‘r's own fatigue detection false-tripped on 60+ instances of the same reusable node class (see `examples/failure_modes/4_recursion_limit.py`). | | **Crash Recovery** | **Corrected**: LangGraph ships a real, automatic checkpointer β€” state is persisted every step, keyed by `thread_id`, and resumes from the last successful step automatically. This is currently *more* automatic than LΓ‘r's own resumability pattern (see next row). | LΓ‘r's state is a plain serializable dict, so save/restore is simple to build β€” but the current examples require the developer to manually track and hardcode the resume entry point (see `examples/patterns/9_resumable_graph.py`). Not automatic yet. | diff --git a/docs/index.md b/docs/index.md index a2f7988..673bb29 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ Every design decision β€” deterministic graphs, step-level state diffs, cryptogr ## Compliance by Design -LΓ‘r ships a complete, production-ready **Enterprise Compliance Backbone** β€” 20 compliance primitives in `lar.compliance` (verified by direct count during this session's independent audit; corrected from an earlier "13," and the "first agentic framework" claim removed as not independently verifiable): +LΓ‘r ships a complete, production-ready **Enterprise Compliance Backbone** β€” 22 compliance primitives in `lar.compliance`: | Primitive | EU AI Act Article | |---|---| @@ -58,7 +58,7 @@ LΓ‘r supports **DeepSeek R1**, **OpenAI o1**, and **Liquid Thinking** out of the ## EU AI Act Ready (August 2026 Enforcement) -LΓ‘r ships a complete, production-ready **Compliance Backbone** β€” 20 primitives in `lar.compliance`, including: +LΓ‘r ships a complete, production-ready **Compliance Backbone** β€” 22 primitives in `lar.compliance`, including: * **Immutable Audit Trails (Art. 12)**: Cryptographically signed causal traces. * **Action-Level Authority Ledger (Art. 14)**: The "Fourth Tier" of human oversight tracking.