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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
27 changes: 27 additions & 0 deletions docs/assets/video/OME-MemoryGuard-Demo-75s.srt
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 43 additions & 0 deletions docs/assets/video/YOUTUBE_UPLOAD.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
42 changes: 42 additions & 0 deletions scripts/build_demo_narration.ps1
Original file line number Diff line number Diff line change
@@ -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"
180 changes: 180 additions & 0 deletions scripts/build_demo_video.py
Original file line number Diff line number Diff line change
@@ -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()
23 changes: 23 additions & 0 deletions tests/test_architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading