diff --git a/.gitignore b/.gitignore index 95ddc21..757d02b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ dist/ build/ *.egg-info/ *.log + +# Reproducible demo sources are tracked; rendered media stays local. +docs/assets/video/*.mp4 +docs/assets/video/*.wav +docs/assets/video/frames/ diff --git a/docs/assets/video/OME-MemoryGuard-Demo-75s.srt b/docs/assets/video/OME-MemoryGuard-Demo-75s.srt new file mode 100644 index 0000000..606b8f8 --- /dev/null +++ b/docs/assets/video/OME-MemoryGuard-Demo-75s.srt @@ -0,0 +1,27 @@ +1 +00:00:00,000 --> 00:00:08,900 +AI agents can remember generated text for months. One secret or prompt injection can become durable context. MemoryGuard turns remembering into a governed transaction. + +2 +00:00:08,900 --> 00:00:21,700 +The system places a deterministic policy gate between the agent and long-term memory. Every proposal is risk scored, explainable, tenant scoped, and auditable. + +3 +00:00:21,700 --> 00:00:38,200 +A safe preference becomes active and is stored with its audit event in one CockroachDB transaction. Retrieval applies isolation and clearance filters before vector ranking. + +4 +00:00:38,200 --> 00:00:52,400 +A secret-bearing prompt injection is denied. Raw content is never persisted. Only reason codes, content length, and a SHA-256 fingerprint enter the audit trail. + +5 +00:00:52,400 --> 00:01:02,000 +Sensitive context remains pending until a human reviewer approves it. Approval does not bypass retrieval clearance, and every transition stays auditable. + +6 +00:01:02,000 --> 00:01:10,500 +AWS provides the protected serverless runtime and embeddings. CockroachDB keeps governed memory, vector search, and audit evidence together. + +7 +00:01:10,500 --> 00:01:14,910 +This recording uses the deterministic local demo and synthetic data. The remaining live Bedrock quota limitation is disclosed. diff --git a/docs/assets/video/YOUTUBE_UPLOAD.md b/docs/assets/video/YOUTUBE_UPLOAD.md new file mode 100644 index 0000000..665152f --- /dev/null +++ b/docs/assets/video/YOUTUBE_UPLOAD.md @@ -0,0 +1,43 @@ +# YouTube upload metadata + +## Title + +OME MemoryGuard — Governed Agentic Memory on CockroachDB × AWS + +## Description + +OME MemoryGuard is a policy-governed persistent memory layer for AI agents. + +The demo shows three synthetic scenarios: a safe memory stored with atomic audit evidence, a secret-bearing prompt injection denied without persisting raw content, and confidential context activated through human approval and clearance-aware retrieval. + +Built with CockroachDB Cloud, VECTOR(1024), AWS Lambda, API Gateway, Cognito, Bedrock Titan Text Embeddings V2, Secrets Manager, S3, AWS SAM, Python, FastAPI, and SQLAlchemy. + +Repository: https://github.com/Naim-arg/OME-MemoryGuard +Public demo: https://acnvju8rb8.execute-api.eu-central-1.amazonaws.com + +Disclosure: this recording uses the deterministic local demo and synthetic data. The isolated AWS/CockroachDB stack was verified separately. Complete live Bedrock-backed retrieval is not claimed while the applied embedding quota remains zero. + +## Upload settings + +- Audience: No, it is not made for kids. +- Visibility: Unlisted. +- Language: English. +- Captions: upload `OME-MemoryGuard-Demo-75s.srt`. + +## Rebuild locally + +The checked-in scripts and captions are the reproducible source package. Rendered WAV, PNG frames, +and MP4 output are intentionally ignored by Git. + +Requirements: Windows with the English Microsoft Zira SAPI voice, Python 3.12, and FFmpeg available +to MoviePy. + +```powershell +python -m pip install -e ".[media]" +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\build_demo_narration.ps1 +python scripts\build_demo_video.py +``` + +The commands create `docs/assets/video/OME-MemoryGuard-Demo-narration.wav`, six frames under +`docs/assets/video/frames/`, and `docs/assets/video/OME-MemoryGuard-Demo-75s.mp4`. Upload the tracked +SRT separately so judges can enable English captions. diff --git a/pyproject.toml b/pyproject.toml index 5f48aab..980d802 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,10 @@ dev = [ "pytest>=8.3,<9", "ruff>=0.9,<1", ] +media = [ + "moviepy>=2,<3", + "pillow>=10,<13", +] [tool.setuptools] packages = ["ome_memoryguard"] diff --git a/scripts/build_demo_narration.ps1 b/scripts/build_demo_narration.ps1 new file mode 100644 index 0000000..cb4b4ee --- /dev/null +++ b/scripts/build_demo_narration.ps1 @@ -0,0 +1,42 @@ +param( + [string]$OutputPath = "docs\assets\video\OME-MemoryGuard-Demo-narration.wav" +) + +$ErrorActionPreference = "Stop" + +$resolvedOutput = [System.IO.Path]::GetFullPath((Join-Path (Get-Location) $OutputPath)) +$outputDirectory = Split-Path -Parent $resolvedOutput +New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null + +$voice = New-Object -ComObject SAPI.SpVoice +$englishVoice = @($voice.GetVoices() | Where-Object { $_.GetDescription() -like "*Zira*" })[0] +if ($null -ne $englishVoice) { + $voice.Voice = $englishVoice +} +$voice.Rate = 1 +$voice.Volume = 100 + +$stream = New-Object -ComObject SAPI.SpFileStream +$stream.Open($resolvedOutput, 3, $false) +$voice.AudioOutputStream = $stream + +$narration = @" +AI agents can remember generated text for months. One secret or prompt injection can become durable context. OME MemoryGuard turns remembering into a governed transaction. + +The system places a deterministic policy gate between the agent and long-term memory. Every proposal is risk scored, explainable, tenant scoped, and auditable before it can affect future actions. + +A safe preference becomes active and is stored with its audit event in one CockroachDB transaction. Retrieval applies tenant, namespace, lifecycle, expiry, and clearance filters before vector ranking. + +A secret-bearing prompt injection is denied. Raw content is never persisted. The audit trail receives only reason codes, content length, and a SHA-256 fingerprint. + +Sensitive context remains pending until a human reviewer approves it. Approval does not bypass retrieval clearance, and every transition stays auditable. + +AWS provides the Cognito protected serverless runtime, Lambda, API Gateway, encrypted evidence exports, and Bedrock embeddings. CockroachDB keeps governed memory, vector search, and audit evidence together. + +This recording uses the deterministic local demo and synthetic data. The remaining live Bedrock quota limitation is disclosed in the submission. +"@ + +[void]$voice.Speak($narration) +$stream.Close() + +Write-Output "NARRATION_PATH=$resolvedOutput" diff --git a/scripts/build_demo_video.py b/scripts/build_demo_video.py new file mode 100644 index 0000000..7032c6a --- /dev/null +++ b/scripts/build_demo_video.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import argparse +import wave +from pathlib import Path + +from moviepy import AudioFileClip, ImageClip, concatenate_videoclips +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[1] +ASSETS = ROOT / "docs" / "assets" +VIDEO_DIR = ASSETS / "video" +FRAME_DIR = VIDEO_DIR / "frames" + + +SLIDES = [ + ( + ASSETS / "architecture.png", + "Governed persistent memory for AI agents", + "Policy first. Durable context only after a deterministic safety decision.", + 0.12, + ), + ( + ASSETS / "screenshots" / "01-overview.png", + "Memory becomes a governed transaction", + "Every proposal is risk-scored, explainable, tenant-scoped, and auditable.", + 0.17, + ), + ( + ASSETS / "screenshots" / "02-safe-retrieval.png", + "Safe memory and atomic evidence", + "CockroachDB stores the active memory and its audit event in one transaction.", + 0.22, + ), + ( + ASSETS / "screenshots" / "03-denied-injection.png", + "Prompt injection denied — raw content blocked", + "Only reason codes, content length, and a SHA-256 fingerprint are audited.", + 0.19, + ), + ( + ASSETS / "screenshots" / "04-review-audit.png", + "Human approval and clearance-aware retrieval", + "Sensitive context stays hidden until approval; every transition remains auditable.", + 0.20, + ), + ( + ASSETS / "architecture.png", + "CockroachDB × AWS", + "Synthetic deterministic demo. The remaining Bedrock quota limitation is disclosed.", + 0.10, + ), +] + + +def font(name: str, size: int) -> ImageFont.FreeTypeFont: + path = Path("C:/Windows/Fonts") / name + return ImageFont.truetype(str(path), size=size) + + +def fit_image(source: Image.Image, box: tuple[int, int]) -> Image.Image: + width, height = box + scale = min(width / source.width, height / source.height) + target = (max(1, int(source.width * scale)), max(1, int(source.height * scale))) + return source.resize(target, Image.Resampling.LANCZOS) + + +def wrap(draw: ImageDraw.ImageDraw, text: str, face: ImageFont.FreeTypeFont, max_width: int) -> list[str]: + lines: list[str] = [] + for paragraph in text.splitlines() or [text]: + words = paragraph.split() + current = "" + for word in words: + candidate = f"{current} {word}".strip() + if draw.textbbox((0, 0), candidate, font=face)[2] <= max_width: + current = candidate + else: + if current: + lines.append(current) + current = word + if current: + lines.append(current) + return lines + + +def render_frame(source_path: Path, title: str, caption: str, destination: Path) -> None: + canvas = Image.new("RGB", (1920, 1080), "#061922") + draw = ImageDraw.Draw(canvas) + title_font = font("seguisb.ttf", 50) + caption_font = font("segoeui.ttf", 30) + brand_font = font("seguisb.ttf", 25) + + draw.rectangle((0, 0, 1920, 8), fill="#20d4c5") + draw.text((72, 32), "OME MemoryGuard", font=brand_font, fill="#20d4c5") + draw.text((72, 74), title, font=title_font, fill="#f2f8fa") + + image_box = (1776, 760) + with Image.open(source_path) as source: + fitted = fit_image(source.convert("RGB"), image_box) + x = (1920 - fitted.width) // 2 + y = 165 + (image_box[1] - fitted.height) // 2 + canvas.paste(fitted, (x, y)) + + draw.rounded_rectangle((48, 932, 1872, 1043), radius=18, fill="#0c2a35", outline="#1a5662", width=2) + caption_lines = wrap(draw, caption, caption_font, 1740) + line_height = 39 + total_height = line_height * len(caption_lines) + caption_y = 988 - total_height // 2 + for line in caption_lines: + draw.text((90, caption_y), line, font=caption_font, fill="#d5e8ed") + caption_y += line_height + + destination.parent.mkdir(parents=True, exist_ok=True) + canvas.save(destination, format="PNG", optimize=True) + + +def wav_duration(path: Path) -> float: + with wave.open(str(path), "rb") as audio: + return audio.getnframes() / audio.getframerate() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--audio", + type=Path, + default=VIDEO_DIR / "OME-MemoryGuard-Demo-narration.wav", + ) + parser.add_argument( + "--output", + type=Path, + default=VIDEO_DIR / "OME-MemoryGuard-Demo-75s.mp4", + ) + args = parser.parse_args() + + audio_path = args.audio.resolve() + output_path = args.output.resolve() + if not audio_path.exists(): + raise SystemExit(f"Narration is missing: {audio_path}") + + duration = wav_duration(audio_path) + if not 55 <= duration <= 100: + raise SystemExit(f"Unexpected narration duration: {duration:.2f}s") + + rendered: list[tuple[Path, float]] = [] + for index, (source, title, caption, weight) in enumerate(SLIDES, start=1): + frame = FRAME_DIR / f"{index:02d}.png" + render_frame(source, title, caption, frame) + rendered.append((frame, duration * weight)) + + clips = [ImageClip(str(frame)).with_duration(slide_duration) for frame, slide_duration in rendered] + video = concatenate_videoclips(clips, method="compose") + audio = AudioFileClip(str(audio_path)) + final = video.with_audio(audio) + + output_path.parent.mkdir(parents=True, exist_ok=True) + final.write_videofile( + str(output_path), + fps=30, + codec="libx264", + audio_codec="aac", + bitrate="4500k", + audio_bitrate="160k", + preset="medium", + threads=4, + logger="bar", + ) + + audio.close() + final.close() + video.close() + for clip in clips: + clip.close() + + print(f"VIDEO_PATH={output_path}") + print(f"DURATION_SECONDS={duration:.2f}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_architecture.py b/tests/test_architecture.py index ea0aecd..149022d 100644 --- a/tests/test_architecture.py +++ b/tests/test_architecture.py @@ -86,6 +86,29 @@ def test_local_artifacts_are_ignored(self) -> None: self.assertIn("*.log", gitignore) self.assertIn(".aws-sam/", gitignore) self.assertIn(".venv/", gitignore) + self.assertIn("docs/assets/video/*.mp4", gitignore) + self.assertIn("docs/assets/video/*.wav", gitignore) + self.assertIn("docs/assets/video/frames/", gitignore) + + def test_demo_media_sources_are_reproducible(self) -> None: + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + instructions = ( + ROOT / "docs" / "assets" / "video" / "YOUTUBE_UPLOAD.md" + ).read_text(encoding="utf-8") + video_script = (ROOT / "scripts" / "build_demo_video.py").read_text(encoding="utf-8") + narration_script = ( + ROOT / "scripts" / "build_demo_narration.ps1" + ).read_text(encoding="utf-8") + + self.assertIn('media = [', pyproject) + self.assertIn('"moviepy>=2,<3"', pyproject) + self.assertIn('"pillow>=10,<13"', pyproject) + self.assertIn('python -m pip install -e ".[media]"', instructions) + self.assertIn("build_demo_narration.ps1", instructions) + self.assertIn("build_demo_video.py", instructions) + self.assertIn("OME-MemoryGuard-Demo-75s.srt", instructions) + self.assertIn("OME-MemoryGuard-Demo-75s.mp4", video_script) + self.assertIn("OME-MemoryGuard-Demo-narration.wav", narration_script) def test_verification_script_covers_local_quality_tools(self) -> None: script = (ROOT / "scripts" / "verify.ps1").read_text(encoding="utf-8")