From d66039423c0240cea8a25ff15a24bb626f365722 Mon Sep 17 00:00:00 2001 From: "Billy (Barn Foreman)" Date: Fri, 24 Apr 2026 20:50:50 -0500 Subject: [PATCH 1/2] docs: align ZTI doctrine with ZTAP ecosystem --- AGENTS.md | 8 +- PROJECT_STATUS.md | 14 +- README.md | 11 +- ZTI_ZTAP_ALIGNMENT.md | 225 +++++++++++++++++++++++++++++++ resources/principles/boundary.md | 11 +- whitepaper/zti-whitepaper.md | 2 + 6 files changed, 252 insertions(+), 19 deletions(-) create mode 100644 ZTI_ZTAP_ALIGNMENT.md diff --git a/AGENTS.md b/AGENTS.md index a2b8494..7b88d7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ and next-steps source for this repository's website and demo systems. ## Website Source of Truth -The website uses a barn-first, single-artifact workflow. +The website uses a single-source, build-from-source workflow. All agents MUST follow these rules: @@ -56,13 +56,13 @@ All agents MUST follow these rules: is removed from the workflow and MUST NOT be recreated or edited. -5. Barn is the authority for build, verify, and deploy. +5. The local build output is the authority for build and verify. The production host is serve-only. 6. CI is validation-only. It MUST NOT deploy the website. 7. Production deployment is artifact-based and full-replace only. -8. Plesk is serve-only and MUST NOT be edited manually. +8. The production host MUST NOT be edited manually. 9. Any emergency hotfix made outside `/dev/site/_src/` MUST be backported into source immediately or the repo is out of policy. @@ -90,7 +90,7 @@ The response MUST clearly state the directory path being used. Example: -`Current working directory: /home/billyb/workspaces/zerotrustintelligence` +`Current working directory: /path/to/zti` ## Hard Path Validation — Website Changes diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 892210d..c01f391 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,5 +1,9 @@ # Project Status +> **Internal operations document.** This file tracks migration checkpoints and +> site/demo workflow state. It is not ZTI doctrine and is not intended for +> external protocol readers. + ## Current Baseline - Branch: `zti-migration-checkpoint` @@ -9,12 +13,12 @@ ## What Is In Place -- The website is on a barn-first, single-source workflow. +- Website source is managed under a single-source build workflow. - Authored website source lives in `dev/site/_src`. - The website builder lives at `dev/site/build.py`. - The only website artifact is `dev/site/_dist`. -- Barn preview serves the built artifact. -- Plesk is serve-only and is intended to receive full artifact replacement deploys. +- Local development server previews the built artifact. +- Production hosting receives full artifact replacement deploys. - The demo is now a deterministic compiler with canonical artifacts under `resources/demo`. - The landing page now embeds `/assets/zti-demo.mp4`. @@ -37,7 +41,7 @@ ## What Was Done - Removed the legacy `site/` workflow from normal website operations. -- Introduced the barn-first build, verify, and deploy workflow. +- Introduced the single-source build, verify, and deploy workflow. - Added the deterministic demo compiler and proof artifacts. - Added demo ops scripts for record, verify, play, and render. - Replaced the homepage video placeholder with the rendered MP4 asset. @@ -55,7 +59,7 @@ - Make compiler outputs the only authority used by verify paths and helper docs. - Extend CI to validate `zti-demo.cast` and `demo.sha256`. - Decide whether `resources/demo/script.md` remains a maintained artifact or is retired. -- Verify barn-to-Plesk deployment with the MP4 included in the website artifact. +- Verify production deployment with the MP4 included in the website artifact. ## Operator Commands diff --git a/README.md b/README.md index 8b3dbba..0ee3115 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,12 @@ Author: Chad McCormack --- -## Current Project Status +## Repository Status -The website is on the barn-first, single-source workflow and the demo is compiled -deterministically from a canonical session model. +This repository is being organized as the public doctrine and reference library +for the Zero-Trust Intelligence ecosystem. -For the current migration checkpoint, completed work, known consistency gaps, and -the next required actions, read: - -- [PROJECT_STATUS.md](PROJECT_STATUS.md) +For internal project tracking and migration notes, see [PROJECT_STATUS.md](PROJECT_STATUS.md). --- diff --git a/ZTI_ZTAP_ALIGNMENT.md b/ZTI_ZTAP_ALIGNMENT.md new file mode 100644 index 0000000..2fc0e8c --- /dev/null +++ b/ZTI_ZTAP_ALIGNMENT.md @@ -0,0 +1,225 @@ +# ZTI / ZTAP Alignment + +*Keeping the doctrine and the protocol conceptually locked together* + +--- + +## Purpose + +ZTI is the doctrine. ZTAP is the first open protocol that implements that doctrine for +governed agent transactions. For the ecosystem to be coherent, these two must remain +conceptually aligned: every principle stated in ZTI doctrine must have a clear and +consistent expression in ZTAP, and every ZTAP protocol decision must be traceable to a +ZTI principle. + +This document defines that alignment, the shared invariants across both repos, and +the rules that prevent the two from drifting apart. + +--- + +## Core Relationship + +``` +ZTI doctrine + └── ZTAP open protocol ← first open protocol under ZTI doctrine + └── ZTI Core ← one commercial control-plane implementation of ZTAP +``` + +| Layer | Repository | Purpose | +|---|---|---| +| **ZTI** | `bitscon/zti` | Doctrine, governing principles, reference library, threat model | +| **ZTAP** | `bitscon/ztap` | Open transaction protocol, schema, conformance, examples | +| **ZTI Core** | private/commercial | Commercial control-plane implementation — not in either doctrine repo | +| **ZTI Adoption** | `bitscon/zti-adoption` | Public education and marketing — not authoritative | + +**ZTI defines the why. ZTAP defines the how. ZTI Core defines one product. ZTI Adoption explains the story.** + +--- + +## Concept Map + +| ZTI Doctrine Concept | ZTAP Protocol Expression | Notes | +|---|---|---| +| AI output is not a decision | `transaction_request` exists in `created` state; no action taken until control plane authorizes | The envelope captures the proposal; the authorization decision is the governance artifact | +| Models draft; deterministic systems govern | Control plane evaluates policy deterministically; `authorization_status` is the outcome; model confidence is not part of evaluation | The AI can initiate a request; only the control plane can authorize it | +| Trust must be proven, not assumed | "ZTAP does not secure the pipe. ZTAP governs the action." Transport security ≠ transaction authorization; authorization record required | An arrival over a secure channel is not an authorized transaction | +| Verified Decision | `authorization_decision` with `auto_authorized` or `human_approved` status, `policy_refs`, valid `integrity.hash_value` | The decision is the sealed authorization record, not the AI's output | +| Integrity / sealing | `integrity` object on every envelope: RFC 8785 JCS canonicalization + SHA-256 hash of content | Hash of `hash_value` excluded; all other integrity fields included; tamper-evident | +| Lineage / provenance | Hash chain: `request_hash` in every `authorization_decision`, `execution_receipt`, `evidence_record`; append-only audit log | Each envelope references the immutable original request; modifications detectable | +| Audit trail | Append-only, hash-linked control plane audit log; every envelope and receipt retained and queryable | A log records; an audit trail proves | +| Execution boundary | Level 3 (Governed Executor) conformance: must refuse work without valid, unexpired ZTAP authorization record, regardless of transport | The enforcement boundary is at the point of action, not at the network perimeter | +| Fail-closed enforcement | Protocol invariant: missing/invalid/expired authorization = reject; no permissive fallback; `SCHEMA_INVALID`, `ACTOR_UNREGISTERED`, `EXPIRED`, `INTEGRITY_FAILED` | Fail-closed is not a default setting — it is an invariant | +| Pattern Registry | Actor registry + capability registry in the control plane; `registration_ref` on every actor | Defines what classes of action are allowed; closed, explicit | +| Detection (classification) | Control plane evaluates `requested_action.action_type` and `profile` against policy; routes to correct policy evaluation | Deterministic: no inference, no probability | +| Explainability | `policy_refs` and `reason_codes` in `authorization_decision`; `evidence_refs` linking accepted evidence | Every authorization decision records its policy basis | +| Validation | Schema validation, `capability_claims` matching, `requested_capabilities` subset check, integrity hash verification | Admissibility check against declared constraints | +| Human approval | `human_approval_required` → `human_approved` two-envelope flow; `approval_scope` binding (`transaction_id`, `request_hash`, `action_ids`, `target_actor_id`, `expires_at`, `single_use: true`) | Approval is transaction-specific, action-bound, time-limited, single-use by default | +| Break-glass | Break-glass path is governed authorization with elevated evidence; `break_glass_context` on request; incident ref, named approver, compensating controls, post-incident review required | More evidence than standard, not less; no bypass mode | +| Governance-class changes | Governance transactions (actor registration, capability grant, policy modification) require elevated authorization; never auto-authorized | Changes to governance rules are the highest-risk transaction class | +| Evidence / receipts | `evidence_record` envelope type; `execution_receipt` with `verification_results`, `atomicity_result`, `actions_completed` | Every terminal transaction state produces a receipt; no receipt = non-compliant | +| Control plane | ZTAP Control Plane (Level 2 conformance): evaluates policy, issues authorization records, maintains audit trail; does not execute | The entity that governs does not execute; the entity that executes does not self-authorize | +| Policy | `policy_refs` array in `authorization_decision`; determines `authorization_status`; organizational policy is external to the protocol | Policy quality matters; ZTI/ZTAP enforce declared constraints, not policy correctness | +| Registered actors / capabilities | `actor_id`, `role`, `capability_claims`, `registration_ref`, `organization_id` in actor objects; separate registry at the control plane | Roles are governance classifications; `implementation_ref` is metadata, not authority | +| Deterministic verification | Schema validation is machine-verifiable; capability matching is deterministic; `reason_codes` are structured enumerations | No heuristic, no inference, no model judgment in authorization evaluation | + +--- + +## Shared Invariants + +These principles must always remain true across both the ZTI doctrine and the ZTAP protocol. +If either repo introduces content that contradicts any of these, it introduces misalignment. + +1. **AI output is proposal, not authority.** A model's output, however confident or capable, + is a candidate. It does not become a decision until it has passed through a deterministic + verification layer. + +2. **Models draft. Deterministic systems govern.** The generation process does not need to be + deterministic. The governance layer does. + +3. **Integrity is mandatory.** Every governed artifact must be hashable, tamper-detectable, + and verifiable. Encryption is policy-conditional; integrity is not. + +4. **Encryption is policy-conditional, not protocol-mandatory.** ZTAP envelopes are plain JSON + by default and must be human-auditable without decryption. Organizations may layer encryption + on top; the protocol does not require it. + +5. **Execution must verify authorization at the point of action.** Transport security does not + substitute for an authorization record. The enforcement boundary is at the action, not at + the network. + +6. **Roles are governance classifications, not tool or vendor names.** Tool names, model names, + SaaS product names, and vendor identifiers are not protocol roles. Using implementation + identity as role identity makes governance vendor-dependent. + +7. **The control plane governs; executors execute.** These are permanently separate. The + entity that governs does not execute. The entity that executes does not self-authorize. + +8. **Ungoverned messages may exist, but cannot become accepted governed work.** A ZTAP-compliant + executor refuses ungoverned instructions regardless of their origin. Ungoverned messages + do not automatically become authorized by arriving via a trusted channel. + +9. **Fail-closed is an invariant, not a default.** Ambiguity resolves to denial. There is no + permissive fallback mode. + +10. **Receipts and evidence are required for governed outcomes.** An action that produces no + receipt is not a governed action. Governance requires evidence. + +--- + +## Terminology Rules + +### Use in public doctrine docs + +| Preferred term | Where it appears | +|---|---| +| Zero-Trust Intelligence | Full name; use in formal contexts | +| ZTI doctrine | The governing philosophy | +| ZTAP protocol | The Zero Trust Agent Protocol | +| ZTAP transaction | The full governed unit of work | +| ZTAP envelope | The serialized JSON object carrying a transaction event | +| ZTAP receipt | The result envelope at a terminal state | +| control plane | The authority layer that evaluates policy and issues authorization records | +| actor | Any entity participating in a ZTAP transaction | +| role | A protocol-level governance classification | +| capability | A declared, registered ability an actor can exercise | +| evidence | Supporting material submitted to satisfy a policy requirement | +| integrity | Tamper detection via hash; mandatory in both ZTI and ZTAP | +| verified decision | ZTI concept: an AI proposal that has passed deterministic verification | +| authorization record | ZTAP concept: the `authorization_decision` envelope issued by the control plane | + +### Avoid in public doctrine docs + +| Avoid | Reason | +|---|---| +| `barn-first` | Internal infrastructure terminology; not meaningful to external readers | +| `barn`, `workshop`, `homestead` | Local development environment names; confusing and internal | +| `single-source workflow` | Internal build methodology; irrelevant to doctrine | +| Billy-specific language | Billy is a possible future reference case, not a protocol authority | +| Tool names as roles (`codex`, `claude`, `gpt-4`, etc.) | Protocol roles must be product-neutral | +| Implying ZTI Core is already part of this repo | ZTI Core is separate; the open library is here, not ZTI Core | +| Implying ZTAP replaces MCP or A2A | ZTAP complements; it does not compete with or replace | +| Implying encryption is mandatory | Integrity is mandatory; encryption is policy-conditional | +| `bitscon/zerotrustintelligence` URLs | Stale after rename; update to `bitscon/zti` | +| Overpromising future protocol versions | ZTAP v1.0-draft is the current state; do not commit to unspecified future work | + +--- + +## ZTI vs ZTAP Boundary + +ZTI and ZTAP describe related but distinct things. They must not be collapsed. + +**ZTI describes:** +- Why verification is necessary (the doctrine problem) +- The architectural concept of a decision verification layer +- What a Verified Decision is +- The verification pipeline: Pattern Registry → Detection → Explainability → Validation → Integrity → Lineage +- The threat model +- What the `zti` reference library implements + +**ZTAP describes:** +- How governed agent transactions are structured (the protocol solution) +- The transaction lifecycle (created → submitted → evaluated → authorized → ...) +- Specific envelope types and field definitions +- Conformance levels and test criteria +- How the control plane contract is defined +- How hashes are computed and verified +- Reason codes and evidence types + +**The line:** ZTI says "verify before execution." ZTAP says "here is the exact structure of how that verification is requested, authorized, executed, and receipted for agent transactions." + +Do not describe ZTAP in the ZTI doctrine repo as if ZTAP is the entirety of ZTI. ZTAP implements one layer of ZTI for one domain (agent transactions). ZTI is broader. + +Do not describe ZTI in the ZTAP protocol repo as if ZTAP and ZTI are interchangeable terms. Reference ZTI as the doctrine that ZTAP implements. + +--- + +## ZTI Core Boundary + +ZTI Core is a separate commercial product. It is not part of the ZTI doctrine repo or the ZTAP protocol repo. + +**The ZTI doctrine repo may:** +- Mention ZTI Core by name as a commercial control-plane implementation +- Link to ZTI Core when it has a public URL +- Describe the conceptual role of a commercial control plane (without ZTI Core's private details) + +**The ZTI doctrine repo must not:** +- Contain ZTI Core product code +- Describe ZTI Core pricing, seat models, or private roadmap +- Define ZTI Core's private API as if it were the open protocol +- Make any claim that implies ZTI Core is the only compliant control plane + +**The ZTAP protocol repo may:** +- Describe ZTI Core as one possible control-plane implementation of ZTAP +- State that ZTAP is open and any conformant control plane may implement it + +**The ZTAP protocol repo must not:** +- Reference ZTI Core internals +- Tie conformance requirements to ZTI Core-specific behavior + +--- + +## Adoption Site Boundary + +The adoption site (`bitscon/zti-adoption`) is the public education and marketing surface. +It should be updated after the ZTI and ZTAP repos are clean and aligned. + +The adoption site: +- consumes the ZTI and ZTAP doctrine, not the other way around +- is contextual and illustrative, not authoritative +- must not drift into defining protocol terms that conflict with ZTI or ZTAP definitions +- should be updated as the final step after repo cleanup and rename are complete + +--- + +## Open Alignment Risks + +The following items require operator decision or monitoring to maintain alignment: + +| Risk | Location | Status | +|---|---|---| +| Old `bitscon/zerotrustintelligence` URLs in `resources/outreach/` | `resources/outreach/posts.md`, `enterprise.md`, `pilot.md`, `resources/deck/slides.md`, `resources/narrative/60-second-explainer.md` | Not yet updated. These files are scheduled for `zti-adoption` or archive. Update before any external use. | +| `resources/principles/boundary.md` perspective confusion | `resources/principles/boundary.md` | Partially addressed. The file was authored from the adoption site's perspective. Context note added. | +| Schema `$id` URLs | `schemas/audit_report.json`, `schemas/verification_trace.json` | Still reference `bitscon.github.io/zerotrustintelligence/`. Update after GitHub rename. | +| `pyproject.toml` repo URL | `pyproject.toml` | May reference old repo name. Verify and update as part of Phase 2 URL sweep. | +| ZTI whitepaper does not mention ZTAP | `whitepaper/zti-whitepaper.md` | The whitepaper predates ZTAP. It is accurate ZTI doctrine but has no ZTAP reference. A single paragraph addition acknowledging ZTAP as the first protocol under ZTI doctrine would close this gap. Operator decision: add or leave for next pass. | +| `resources/assets/architecture.md` old URL | `resources/assets/architecture.md` | References `bitscon/zerotrustintelligence`. Scheduled for URL sweep. | diff --git a/resources/principles/boundary.md b/resources/principles/boundary.md index f68b00c..7f633ac 100644 --- a/resources/principles/boundary.md +++ b/resources/principles/boundary.md @@ -2,13 +2,18 @@ ## What ZTI Is -Zero Trust Intelligence (ZTI) is a protocol. +Zero Trust Intelligence (ZTI) is a doctrine and verification protocol. -Specifically: a deterministic decision verification protocol and control layer +Specifically: a deterministic decision verification layer and governance philosophy that sits between stochastic AI generation and real-world execution systems. The authoritative definition lives in the protocol repository: -[bitscon/zerotrustintelligence](https://github.com/bitscon/zerotrustintelligence) +[bitscon/zti](https://github.com/bitscon/zti) + +> **Note:** This boundary document was authored from the perspective of the adoption +> site's relationship to the ZTI doctrine repo. It remains here as a principles reference. +> The "this repository" language below refers to any contextual/illustrative repository +> (such as the adoption site), not the ZTI doctrine repo itself. --- diff --git a/whitepaper/zti-whitepaper.md b/whitepaper/zti-whitepaper.md index b119563..da848dc 100644 --- a/whitepaper/zti-whitepaper.md +++ b/whitepaper/zti-whitepaper.md @@ -434,6 +434,8 @@ Remove the need to trust the participant. Enforce verification at the protocol level. +ZTAP — the Zero Trust Agent Protocol — is the first open protocol under the ZTI doctrine. Where ZTI defines the principle that AI outputs and AI-initiated actions must be verified before they are trusted, ZTAP defines a concrete transaction, envelope, receipt, and conformance model for governed agent work. ZTAP lives separately at https://github.com/bitscon/ztap and is currently tagged `v1.0-draft`. + Make proof the only acceptable gate to execution. ZTI is not a blockchain. From 266fc8c079176de3415c848b12e00a6c683616a7 Mon Sep 17 00:00:00 2001 From: "Billy (Barn Foreman)" Date: Fri, 24 Apr 2026 21:06:11 -0500 Subject: [PATCH 2/2] fix: make demo video optional for CI site builds --- dev/site/build.py | 20 ++++++++++++++------ tests/test_site_build.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/dev/site/build.py b/dev/site/build.py index bed2c2d..104d1f0 100644 --- a/dev/site/build.py +++ b/dev/site/build.py @@ -3,6 +3,7 @@ import argparse import hashlib import json +import os import re import shutil import sys @@ -32,9 +33,10 @@ 'assets/site.css', 'assets/site.js', 'assets/demo-output.txt', - 'assets/zti-demo.mp4', ) +TRUTHY_VALUES = {'1', 'true', 'yes', 'on'} + def _load_config() -> dict[str, object]: return json.loads(CONFIG_PATH.read_text(encoding='utf-8')) @@ -144,10 +146,13 @@ def _stage_transcript(output_dir: Path, project_root: Path) -> Path: def _stage_demo_video(output_dir: Path, project_root: Path) -> Path: video_source = project_root / 'resources' / 'demo' / 'build' / 'zti-demo.mp4' + require_demo_video = str(os.getenv('ZTI_REQUIRE_DEMO_VIDEO', '')).strip().lower() in TRUTHY_VALUES if not video_source.exists(): - raise FileNotFoundError( - f'Missing rendered demo video: {video_source}. Run ./ops/render-demo.sh before building the site.' - ) + message = f'Missing rendered demo video: {video_source}. Run ./ops/render-demo.sh before building the site.' + if require_demo_video: + raise FileNotFoundError(message) + print(f'WARNING: {message} Skipping demo video staging (set ZTI_REQUIRE_DEMO_VIDEO=1 to require it).', file=sys.stderr) + return output_dir / 'assets' / 'zti-demo.mp4' destination = output_dir / 'assets' / 'zti-demo.mp4' destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(video_source.read_bytes()) @@ -186,14 +191,17 @@ def _assert_root_relative_paths(output_dir: Path) -> None: raise AssertionError(f'Artifact leaked local-only host reference in {path}') -def _assert_required_files(output_dir: Path) -> None: +def _assert_required_files(output_dir: Path, *, require_demo_video_asset: bool = False) -> None: for rel_path in REQUIRED_ARTIFACT_FILES: if not (output_dir / rel_path).exists(): raise AssertionError(f'Missing required artifact file: {rel_path}') + if require_demo_video_asset and not (output_dir / 'assets' / 'zti-demo.mp4').exists(): + raise AssertionError('Missing required artifact file: assets/zti-demo.mp4') def check_build(project_root: Path | None = None) -> None: project_root = project_root or PROJECT_ROOT + expect_demo_video_asset = (project_root / 'resources' / 'demo' / 'build' / 'zti-demo.mp4').exists() with tempfile.TemporaryDirectory() as tmp: tmp_root = Path(tmp) artifact_one = build_site(tmp_root / 'artifact-one', project_root) @@ -203,7 +211,7 @@ def check_build(project_root: Path | None = None) -> None: raise AssertionError('Site artifact output is not deterministic') _assert_root_relative_paths(artifact_one) - _assert_required_files(artifact_one) + _assert_required_files(artifact_one, require_demo_video_asset=expect_demo_video_asset) def _parser() -> argparse.ArgumentParser: diff --git a/tests/test_site_build.py b/tests/test_site_build.py index eceaf5c..5c42456 100644 --- a/tests/test_site_build.py +++ b/tests/test_site_build.py @@ -1,8 +1,10 @@ from __future__ import annotations +import os import tempfile import unittest from pathlib import Path +from unittest import mock from dev.site.build import build_site, check_build from zti.demo.export import export_terminal_output @@ -27,7 +29,8 @@ def test_build_outputs_root_relative_paths_and_hidden_core_nav(self) -> None: self.assertNotIn('>Core', adopt_html) self.assertIn('href="/core/"', adopt_html) self.assertIn('', index_html) - self.assertTrue((artifact_out / "assets" / "zti-demo.mp4").exists()) + source_video = Path.cwd() / "resources" / "demo" / "build" / "zti-demo.mp4" + self.assertEqual((artifact_out / "assets" / "zti-demo.mp4").exists(), source_video.exists()) def test_build_stages_canonical_transcript_into_output(self) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -38,6 +41,35 @@ def test_build_stages_canonical_transcript_into_output(self) -> None: canonical = export_terminal_output(project_root).read_text(encoding="utf-8") self.assertEqual(transcript_asset, canonical) + def test_missing_demo_video_does_not_fail_by_default(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_root = Path(tmp) + project_root = tmp_root / "project" + project_root.mkdir(parents=True, exist_ok=True) + with mock.patch.dict(os.environ, {}, clear=False): + artifact_out = build_site(tmp_root / "artifact", project_root=project_root) + self.assertFalse((artifact_out / "assets" / "zti-demo.mp4").exists()) + + def test_missing_demo_video_fails_in_strict_mode(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_root = Path(tmp) + project_root = tmp_root / "project" + project_root.mkdir(parents=True, exist_ok=True) + with mock.patch.dict(os.environ, {"ZTI_REQUIRE_DEMO_VIDEO": "1"}, clear=False): + with self.assertRaises(FileNotFoundError): + build_site(tmp_root / "artifact", project_root=project_root) + + def test_existing_demo_video_stages_when_present(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_root = Path(tmp) + project_root = tmp_root / "project" + video_source = project_root / "resources" / "demo" / "build" / "zti-demo.mp4" + video_source.parent.mkdir(parents=True, exist_ok=True) + payload = b"fake-mp4-bytes" + video_source.write_bytes(payload) + artifact_out = build_site(tmp_root / "artifact", project_root=project_root) + self.assertEqual((artifact_out / "assets" / "zti-demo.mp4").read_bytes(), payload) + if __name__ == "__main__": unittest.main()