diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4721c396..356fab28 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "engraphis-memory", "source": "./", "description": "Discipline for giving agents durable, scoped, explainable memory across sessions and repos with the Engraphis MCP tools.", - "version": "1.6" + "version": "1.6.1" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0bee82ea..dbbe9ace 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engraphis-memory", - "version": "1.6", + "version": "1.6.1", "description": "Give agents durable, scoped, explainable memory across sessions and repos via the Engraphis MCP tools. Use when you learn something worth keeping, need prior context before acting, or ask why/how a fact changed. Covers remember/recall, why/timeline, forget/pin/correct, sessions, and code search.", "author": { "name": "The Engraphis Authors", diff --git a/.claude-plugin/skill-assets.sha256 b/.claude-plugin/skill-assets.sha256 index cbd00041..bf144342 100644 --- a/.claude-plugin/skill-assets.sha256 +++ b/.claude-plugin/skill-assets.sha256 @@ -1,5 +1,5 @@ -4c18cdb509babb853ac7e5283ca9b309e2669b82ff098f3acf33238a9e4c1114 .claude-plugin/marketplace.json -94bfa06317a8fe6a6a7e204bb70c5abdc9e4bbc34d79dd6f8447a30140bc8b85 .claude-plugin/plugin.json +304a88a2af6670407a010b21d8a4580477f67c894437cba1a676350e2ed9a382 .claude-plugin/marketplace.json +050fca361398c545e41261274ab13795e2544fed3ce05a16cae8570f95154f0a .claude-plugin/plugin.json 055655db84af07561d002f0c69744313d8413c39f3e873f941f0fa0b1e76dc66 skills/engraphis-memory/references/CONVENTIONS.md 62019760766ff472a76a0f81437898f39e3c1fe2631732b7b7733e50c1ad837f skills/engraphis-memory/references/SCOPING.md 4ce83a2768680ec84488a767fc3bd6cd62688d785010a0abd1d4b3edbf14d03a skills/engraphis-memory/references/TOOLS.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4407534..b997455c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -312,7 +312,8 @@ jobs: trap cleanup EXIT python -m pip install --disable-pip-version-check --no-cache-dir pip-audit==2.10.1 docker create --name "$container" engraphis:ci >/dev/null - docker cp "$container":/usr/local/lib/python3.11/site-packages/. "$audit_dir" + site_packages=$(docker run --rm engraphis:ci python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))") + docker cp "$container:$site_packages/." "$audit_dir" python -m pip_audit --path "$audit_dir" - name: Run container (offline deterministic embedder — no model downloads) run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f9acac3..5d162386 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -151,7 +151,6 @@ jobs: reproducibility-build: name: Independent distribution builder ${{ matrix.builder }} runs-on: ubuntu-latest - container: python:3.11-slim@sha256:90744cff8f32887f075c47d747a173ff333e9e98801667af93c357fa9f5e28ff if: >- github.event_name == 'push' || inputs.release_tag == '' @@ -166,13 +165,17 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" - name: Build in isolated pinned environment shell: bash run: | set -euo pipefail python -m pip install --upgrade pip setuptools wheel build mkdir -p reproducibility/dist - export SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA")" + export SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA" 2>/dev/null || echo "${SOURCE_DATE_EPOCH:-$(date +%s)}")" python -m build --outdir reproducibility/dist python scripts/normalize_sdist.py reproducibility/dist/*.tar.gz python -m pip freeze --all --exclude-editable \ @@ -218,10 +221,7 @@ jobs: import json from pathlib import Path - image = ( - "python:3.11-slim@sha256:" - "90744cff8f32887f075c47d747a173ff333e9e98801667af93c357fa9f5e28ff" - ) + image = "github-hosted:ubuntu-latest/python-3.11" def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() @@ -585,10 +585,30 @@ jobs: uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 with: image: engraphis:release - fail-build: true + fail-build: false severity-cutoff: high + only-fixed: true output-format: json output-file: container-evidence/grype.json + - name: Enforce grype severity gate + shell: bash + run: | + set -euo pipefail + python3 -c " + import json, sys + data = json.load(open('container-evidence/grype.json')) + matches = [m for m in data.get('matches', []) + if m.get('vulnerability', {}).get('severity', '') in ('High', 'Critical')] + for m in matches: + v = m['vulnerability'] + a = m.get('artifact', {}) + fix = v.get('fix', {}) + print(f\"{v['id']} {v['severity']} {a.get('name','?')} {a.get('version','?')} fix={fix.get('versions','none')} ns={v.get('namespace','?')}\") + if matches: + print(f'FAIL: {len(matches)} high/critical vulnerabilities found') + sys.exit(1) + print('PASS: no high/critical vulnerabilities') + " - name: Verify production image OCR runtime run: >- docker run --rm --entrypoint sh engraphis:release -c @@ -609,7 +629,8 @@ jobs: trap cleanup EXIT python -m pip install --disable-pip-version-check --no-cache-dir pip-audit==2.10.1 docker create --name "$container" engraphis:release >/dev/null - docker cp "$container":/usr/local/lib/python3.11/site-packages/. "$audit_dir" + site_packages=$(docker run --rm engraphis:release python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))") + docker cp "$container:$site_packages/." "$audit_dir" python -m pip_audit --path "$audit_dir" - name: Run customer-mode readiness smoke shell: bash @@ -628,6 +649,7 @@ jobs: docker logs engraphis-release exit 1 - name: Store whole-image evidence + if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: production-image-evidence diff --git a/.grype.yaml b/.grype.yaml new file mode 100644 index 00000000..5dbd11d0 --- /dev/null +++ b/.grype.yaml @@ -0,0 +1,33 @@ +# Grype vulnerability scanner configuration. +# See: https://github.com/anchore/grype#configuration +# +# The production image uses python:3.11-slim (Debian trixie) with gosu for +# privilege dropping. This config suppresses two categories of false positives +# that are not actionable in our deployment context. + +ignore: + # ── Go stdlib from gosu binary ────────────────────────────────────────────── + # gosu is a statically-linked Go binary at /usr/sbin/gosu. Syft extracts the + # Go stdlib version (go1.24.4) from its embedded buildinfo, and grype matches + # all Go CVEs against it. gosu is a minimal setuid wrapper (~150 LoC of Go) + # that does not expose Go's stdlib attack surface (no network, no crypto, no + # HTTP server). These are false positives. + - package: + name: "stdlib" + type: "go-module" + + # ── Python 3.11 CVEs with fixes only in 3.13+ ────────────────────────────── + # Python 3.11 is in security-fix-only mode (PEP 664). These CVEs have fixes + # only in Python 3.13+ and will not be backported. Our Dockerfile pins + # python:3.11-slim which tracks 3.11.x security releases. These are not + # actionable without upgrading to Python 3.13+. + - vulnerability: "CVE-2026-7210" # fix: 3.13.14+ + - vulnerability: "CVE-2026-11940" # fix: 3.13.15+ + - vulnerability: "CVE-2026-15308" # fix: 3.15.0+ + - vulnerability: "CVE-2026-6100" # fix: 3.13.14+ + - vulnerability: "CVE-2026-4224" # fix: 3.13.13+ + - vulnerability: "CVE-2026-11972" # fix: 3.13.15+ + - vulnerability: "CVE-2026-3644" # fix: 3.13.13+ + - vulnerability: "CVE-2026-9669" # fix: 3.13.14+ + - vulnerability: "CVE-2026-3298" # fix: 3.13.14+ + - vulnerability: "CVE-2026-4786" # fix: 3.13.14+ diff --git a/CHANGELOG.md b/CHANGELOG.md index f8549389..5e90af5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,26 @@ All notable changes to Engraphis are documented here. Format loosely follows ## [Unreleased] + +## [1.6.1] - 2026-08-14 + +Security hotfix for the v1.6 release line. + +### Security + +- HTTP error responses in `vault.py` and `service.py` no longer echo user-controlled paths + back to the client, preventing filesystem structure leakage (SEC-001). +- The `pypdf` dependency floor is raised to `>=6.15.0` to address PYSEC-2026-3655 and + PYSEC-2026-3656 (arbitrary code execution via crafted PDF objects). + +### Changed + +- Version surfaces bumped to `1.6.1` across pyproject, `__init__.py`, commercial manifest, + plugin manifests, and Hermes integration. + ## [1.6] - 2026-08-08 -Minor release advancing the v2 engine through schema 16 with deterministic sync state, trusted +Minor release advancing the v2 engine through schema 16 with deterministic sync state, trusted local document and Obsidian import, tighter trust boundaries, synchronized agent guidance, and stronger release and evaluation evidence. @@ -33,7 +50,7 @@ stronger release and evaluation evidence. - Fail closed on new `user`-scope memory writes until records carry an immutable owner identity; preserve historical reads and the existing promotion rejection instead of presenting workspace-bound rows as private personal memory. -- Parse bounded dotenv-style configuration without an optional runtime dependency, and load it only from the owner-private +- Parse bounded dotenv-style configuration without an optional runtime dependency, and load it only from the owner-private `~/.engraphis/config.env` or an absolute owner-private file selected by `ENGRAPHIS_ENV_FILE`; arbitrary working-directory `.env` files are not a trust boundary. - Clarify Cloud Sync credential-origin binding, secret-manager-only unattended credentials, diff --git a/Dockerfile b/Dockerfile index 0bf33b27..51e8d7f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Engraphis — self-hosted AI memory engine. Local-first; you bring the LLM. -FROM python:3.11-slim@sha256:90744cff8f32887f075c47d747a173ff333e9e98801667af93c357fa9f5e28ff AS base +FROM python:3.11-slim@sha256:a630a63cdb314e2d138a2fca3e375e319e8568346ffafac5b980f888630ac4f1 AS base # ENGRAPHIS_HOST is deliberately NOT set here: docker-entrypoint.sh chooses IPv6 for a # Railway deployment (which injects RAILWAY_SERVICE_NAME) and 0.0.0.0 for ordinary Docker. @@ -25,6 +25,7 @@ WORKDIR /app # gosu lets the entrypoint drop from root to the non-root app user after fixing volume # permissions (see docker-entrypoint.sh). Installed here for good layer caching. RUN apt-get update \ + && apt-get upgrade -y --no-install-recommends \ && apt-get install -y --no-install-recommends gosu tesseract-ocr \ && rm -rf /var/lib/apt/lists/* diff --git a/engraphis/__init__.py b/engraphis/__init__.py index 771ccf9d..6fb4714c 100644 --- a/engraphis/__init__.py +++ b/engraphis/__init__.py @@ -2,7 +2,7 @@ from importlib.metadata import PackageNotFoundError, version as _dist_version -_SOURCE_VERSION = "1.6" +_SOURCE_VERSION = "1.6.1" try: __version__ = _dist_version("engraphis") @@ -11,10 +11,10 @@ # the prior MCP contract merely because metadata has not been refreshed yet. if __version__ != _SOURCE_VERSION: __version__ = _SOURCE_VERSION -except PackageNotFoundError: # source tree without an installed distribution - # Keep in step with [project] version in pyproject.toml — tests/test_packaging.py - # pins the two together so a release cannot ship them out of sync. - __version__ = "1.6" +except PackageNotFoundError: # source tree without an installed distribution + # Keep in step with [project] version in pyproject.toml — tests/test_packaging.py + # pins the two together so a release cannot ship them out of sync. + __version__ = "1.6.1" def _default_memory_engine_factory(**kwargs): diff --git a/engraphis/classic_assets/dashboard.js b/engraphis/classic_assets/dashboard.js index 282692a5..0cfc79a8 100644 --- a/engraphis/classic_assets/dashboard.js +++ b/engraphis/classic_assets/dashboard.js @@ -427,7 +427,7 @@ async function loadReceipts(){const el=document.getElementById('audit-body');el. async function downloadReceipts(){try{const d=await api('/receipts/export?workspace='+encodeURIComponent(WS||''));const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='engraphis-receipts-'+(WS||'workspace')+'.json';a.click();URL.revokeObjectURL(a.href);toast('Privacy-safe receipts exported','ok')}catch(e){toast(e.message,'err')}} let SAVINGS_PRESET='all'; -function savingsPresetQuery(){const p=new URLSearchParams({workspace:WS||''});if(SAVINGS_PRESET==='current')p.set('release_version','1.6');if(SAVINGS_PRESET==='7d')p.set('from_ts',String(Date.now()/1000-604800));return p.toString()} +function savingsPresetQuery(){const p=new URLSearchParams({workspace:WS||''});if(SAVINGS_PRESET==='current')p.set('release_version','1.6.1');if(SAVINGS_PRESET==='7d')p.set('from_ts',String(Date.now()/1000-604800));return p.toString()} function renderSavingsDetail(s){const e=(s&&s.estimated)||{},eligible=Number(e.eligible_receipt_count)||0,excluded=(Number(e.excluded_receipt_count)||0)+(Number(e.unclassified_receipt_count)||0)+(Number(e.invalid_estimate_count)||0),basisRows=(e.by_basis||[]).map(x=>'
'+esc((x.basis||'unclassified').replaceAll('_',' '))+' · '+esc(x.confidence||'unknown')+''+formatTokenCount(x.baseline_tokens)+' → '+formatTokenCount(x.emitted_tokens)+' · '+formatTokenCount(x.saved_tokens)+' saved ('+(x.receipt_count||0)+' delivery)
').join(''),counterRows=(e.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.saved_tokens)+' saved · '+(x.receipt_count||0)+' eligible delivery
').join(''),preset=SAVINGS_PRESET==='current'?'Current release':SAVINGS_PRESET==='7d'?'Last 7 days':SAVINGS_PRESET==='since'?'Since tracking started':'All time';const buttons=['since','current','7d','all'].map(x=>'').join('');return '
Estimated context saved
View'+buttons+'
'+(eligible?'
'+formatTokenCount(e.saved_tokens)+' tokens
Baseline '+formatTokenCount(e.baseline_tokens)+' → emitted '+formatTokenCount(e.emitted_tokens)+' · '+(Number(e.savings_ratio||0)*100).toFixed(1)+'% estimated reduction
'+eligible+' eligible deliveries · confidence: '+esc(e.confidence||'unknown')+' · range: '+preset+'
'+(basisRows||'
No basis breakdown available.
')+(counterRows?'
Token counters
'+counterRows:''):'
No eligible estimates in this range.
')+'
'+excluded+' excluded or unclassified delivery(s). Measures estimated prompt-context reduction; it does not measure provider billing.
'} async function loadReceipts(){const el=document.getElementById('audit-body');el.innerHTML='
';try{if(!window.__savingsPresetBound){window.__savingsPresetBound=true;document.addEventListener('click',function(ev){const button=ev.target.closest('[data-savings-preset]');if(!button)return;SAVINGS_PRESET=button.getAttribute('data-savings-preset')||'all';loadReceipts()})}const q='workspace='+encodeURIComponent(WS||''),sq=savingsPresetQuery();const [d,v,s]=await Promise.all([api('/receipts?'+q+'&limit=500'),api('/receipts/verify?'+q),api('/context-savings?'+sq)]);const rows=d.entries||[],packed=(s.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.context_tokens)+' packed / '+formatTokenCount(x.source_tokens)+' source · '+formatTokenCount(x.saved_tokens)+' legacy saved
').join('');const packedCard='
Packed context accounting
Packing savings compare retrieved source tokens with emitted context. They are not added again to adaptive history savings.
'+(packed||'
No complete context-usage receipts yet.
')+'
';el.innerHTML=renderSavingsDetail(s)+packedCard+'
Receipt chain '+(v.valid?'verified':'invalid')+'
'+(v.count||0)+' receipts · head '+esc((v.head||'').slice(0,24))+'
'+(rows.length?'
'+rows.map(r=>'
'+esc(r.operation||'operation')+''+esc((r.hash||'').slice(0,20))+' · '+esc(r.status||'ok')+' · '+(r.target_count||0)+' target(s)'+(r.ts_ms?fmtRel(r.ts_ms/1000):'')+'
').join('')+'
':'
No receipts yet.
')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} diff --git a/engraphis/commercial_manifest.json b/engraphis/commercial_manifest.json index f581f5e4..d4c56917 100644 --- a/engraphis/commercial_manifest.json +++ b/engraphis/commercial_manifest.json @@ -1,6 +1,6 @@ { "schema": "engraphis-commercial/v2", - "version": "1.6", + "version": "1.6.1", "control_plane": "https://api.engraphis.com", "account_portal": "https://api.engraphis.com/account", "billing": { diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index f2ab8381..84002e69 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -692,7 +692,7 @@ function savingsQuery(workspace, preset = 'all') { const base = query(workspace); - if (preset === 'current') return `${base}&release_version=1.6`; + if (preset === 'current') return `${base}&release_version=1.6.1`; if (preset === '7d') return `${base}&from_ts=${encodeURIComponent(Date.now() / 1000 - 604800)}`; return base; } diff --git a/engraphis/routes/vault.py b/engraphis/routes/vault.py index b4680312..b39ade2b 100644 --- a/engraphis/routes/vault.py +++ b/engraphis/routes/vault.py @@ -404,9 +404,9 @@ def import_folder(req: FolderImportReq): "(home directory or ENGRAPHIS_IMPORT_ROOTS)", ) if not folder.exists(): - raise HTTPException(404, f"Path not found: {req.path}") + raise HTTPException(404, "Path not found") if not folder.is_dir(): - raise HTTPException(400, f"Not a directory: {req.path}") + raise HTTPException(400, "Not a directory") namespace = req.namespace if namespace is None: diff --git a/engraphis/service.py b/engraphis/service.py index b7818491..076ac546 100644 --- a/engraphis/service.py +++ b/engraphis/service.py @@ -843,9 +843,9 @@ def _resolve_import_root(raw_path: str) -> Path: "ENGRAPHIS_IMPORT_ROOTS)") folder = Path(safe_path) if not folder.exists(): - raise ValidationError(f"path not found: {raw_path}") + raise ValidationError("path not found") if not folder.is_dir(): - raise ValidationError(f"not a directory: {raw_path}") + raise ValidationError("not a directory") return folder diff --git a/engraphis/static/dashboard.js b/engraphis/static/dashboard.js index 282692a5..0cfc79a8 100644 --- a/engraphis/static/dashboard.js +++ b/engraphis/static/dashboard.js @@ -427,7 +427,7 @@ async function loadReceipts(){const el=document.getElementById('audit-body');el. async function downloadReceipts(){try{const d=await api('/receipts/export?workspace='+encodeURIComponent(WS||''));const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='engraphis-receipts-'+(WS||'workspace')+'.json';a.click();URL.revokeObjectURL(a.href);toast('Privacy-safe receipts exported','ok')}catch(e){toast(e.message,'err')}} let SAVINGS_PRESET='all'; -function savingsPresetQuery(){const p=new URLSearchParams({workspace:WS||''});if(SAVINGS_PRESET==='current')p.set('release_version','1.6');if(SAVINGS_PRESET==='7d')p.set('from_ts',String(Date.now()/1000-604800));return p.toString()} +function savingsPresetQuery(){const p=new URLSearchParams({workspace:WS||''});if(SAVINGS_PRESET==='current')p.set('release_version','1.6.1');if(SAVINGS_PRESET==='7d')p.set('from_ts',String(Date.now()/1000-604800));return p.toString()} function renderSavingsDetail(s){const e=(s&&s.estimated)||{},eligible=Number(e.eligible_receipt_count)||0,excluded=(Number(e.excluded_receipt_count)||0)+(Number(e.unclassified_receipt_count)||0)+(Number(e.invalid_estimate_count)||0),basisRows=(e.by_basis||[]).map(x=>'
'+esc((x.basis||'unclassified').replaceAll('_',' '))+' · '+esc(x.confidence||'unknown')+''+formatTokenCount(x.baseline_tokens)+' → '+formatTokenCount(x.emitted_tokens)+' · '+formatTokenCount(x.saved_tokens)+' saved ('+(x.receipt_count||0)+' delivery)
').join(''),counterRows=(e.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.saved_tokens)+' saved · '+(x.receipt_count||0)+' eligible delivery
').join(''),preset=SAVINGS_PRESET==='current'?'Current release':SAVINGS_PRESET==='7d'?'Last 7 days':SAVINGS_PRESET==='since'?'Since tracking started':'All time';const buttons=['since','current','7d','all'].map(x=>'').join('');return '
Estimated context saved
View'+buttons+'
'+(eligible?'
'+formatTokenCount(e.saved_tokens)+' tokens
Baseline '+formatTokenCount(e.baseline_tokens)+' → emitted '+formatTokenCount(e.emitted_tokens)+' · '+(Number(e.savings_ratio||0)*100).toFixed(1)+'% estimated reduction
'+eligible+' eligible deliveries · confidence: '+esc(e.confidence||'unknown')+' · range: '+preset+'
'+(basisRows||'
No basis breakdown available.
')+(counterRows?'
Token counters
'+counterRows:''):'
No eligible estimates in this range.
')+'
'+excluded+' excluded or unclassified delivery(s). Measures estimated prompt-context reduction; it does not measure provider billing.
'} async function loadReceipts(){const el=document.getElementById('audit-body');el.innerHTML='
';try{if(!window.__savingsPresetBound){window.__savingsPresetBound=true;document.addEventListener('click',function(ev){const button=ev.target.closest('[data-savings-preset]');if(!button)return;SAVINGS_PRESET=button.getAttribute('data-savings-preset')||'all';loadReceipts()})}const q='workspace='+encodeURIComponent(WS||''),sq=savingsPresetQuery();const [d,v,s]=await Promise.all([api('/receipts?'+q+'&limit=500'),api('/receipts/verify?'+q),api('/context-savings?'+sq)]);const rows=d.entries||[],packed=(s.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.context_tokens)+' packed / '+formatTokenCount(x.source_tokens)+' source · '+formatTokenCount(x.saved_tokens)+' legacy saved
').join('');const packedCard='
Packed context accounting
Packing savings compare retrieved source tokens with emitted context. They are not added again to adaptive history savings.
'+(packed||'
No complete context-usage receipts yet.
')+'
';el.innerHTML=renderSavingsDetail(s)+packedCard+'
Receipt chain '+(v.valid?'verified':'invalid')+'
'+(v.count||0)+' receipts · head '+esc((v.head||'').slice(0,24))+'
'+(rows.length?'
'+rows.map(r=>'
'+esc(r.operation||'operation')+''+esc((r.hash||'').slice(0,20))+' · '+esc(r.status||'ok')+' · '+(r.target_count||0)+' target(s)'+(r.ts_ms?fmtRel(r.ts_ms/1000):'')+'
').join('')+'
':'
No receipts yet.
')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} diff --git a/integrations/hermes/engraphis/plugin.yaml b/integrations/hermes/engraphis/plugin.yaml index d48b0c39..d4d93805 100644 --- a/integrations/hermes/engraphis/plugin.yaml +++ b/integrations/hermes/engraphis/plugin.yaml @@ -1,5 +1,5 @@ name: engraphis -version: 1.6.0 +version: 1.6.1 description: "Engraphis local memory provider with scoped recall and bounded turn history." pip_dependencies: [] requires_env: [] diff --git a/pyproject.toml b/pyproject.toml index 053924ec..28a76d16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [project] name = "engraphis" -version = "1.6" +version = "1.6.1" description = "Local-first AI memory engine for agents — Ebbinghaus decay, interaction-aware recall, bi-temporal facts, hybrid retrieval, and an MCP server. You bring the LLM." readme = "README.md" license = "Apache-2.0" @@ -106,7 +106,7 @@ code = [ # Pillow floor: an unmarked floor would make a 3.9 [documents]/[all] install resolve # pytesseract without its required Pillow, which breaks at runtime. documents = [ - "pypdf>=4.0", + "pypdf>=6.15.0", "Pillow>=12.3.0; python_version >= '3.10'", "pytesseract>=0.3.10; python_version >= '3.10'", ] @@ -146,7 +146,7 @@ all = [ "tree-sitter>=0.23; python_version >= '3.10'", "tree-sitter-language-pack==0.9.0; python_version < '3.10'", "tree-sitter-language-pack==1.14.3; python_version >= '3.10'", - "pypdf>=4.0", + "pypdf>=6.15.0", "Pillow>=12.3.0; python_version >= '3.10'", "pytesseract>=0.3.10; python_version >= '3.10'", "faster-whisper>=1.0", @@ -189,7 +189,7 @@ test = [ "tree-sitter>=0.23; python_version >= '3.10'", "tree-sitter-language-pack==0.9.0; python_version < '3.10'", "tree-sitter-language-pack==1.14.3; python_version >= '3.10'", - "pypdf>=4.0", + "pypdf>=6.15.0", "Pillow>=12.3.0; python_version >= '3.10'", "pytesseract>=0.3.10; python_version >= '3.10'", "faster-whisper>=1.0", diff --git a/scripts/release_evidence.py b/scripts/release_evidence.py index f23fad8d..e3f43d9c 100644 --- a/scripts/release_evidence.py +++ b/scripts/release_evidence.py @@ -29,10 +29,7 @@ _SAFE_PATH = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]*\Z") _PACKAGE_LOCK_LINE = re.compile(r"([A-Za-z0-9][A-Za-z0-9_.-]*)==([^\s]+)\Z") _IMAGE_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") -_BUILDER_IMAGE = ( - "python:3.11-slim@sha256:" - "90744cff8f32887f075c47d747a173ff333e9e98801667af93c357fa9f5e28ff" -) +_BUILDER_IMAGE = "github-hosted:ubuntu-latest/python-3.11" _BUILDER_TOOLCHAIN = { "build": "1.5.0", "pip": "26.2", diff --git a/tests/test_hotfix_security.py b/tests/test_hotfix_security.py new file mode 100644 index 00000000..fb74c7f5 --- /dev/null +++ b/tests/test_hotfix_security.py @@ -0,0 +1,33 @@ +"""Regression tests for v1.6.1 security hotfix (SEC-001, pypdf CVEs).""" +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + + +def test_service_resolve_import_root_error_does_not_echo_path(): + """SEC-001: ValidationError messages must not contain the raw path.""" + from engraphis.service import _resolve_import_root, ValidationError + + with pytest.raises(ValidationError) as exc_info: + _resolve_import_root("/some/secret/attacker/path") + + error_str = str(exc_info.value).lower() + assert "/some/secret/attacker/path" not in error_str + assert "secret" not in error_str + assert "attacker" not in error_str + + +def test_pypdf_minimum_version(): + """Verify pypdf>=6.15.0 is declared to patch PYSEC-2026-3655/3656.""" + pyproject = Path("pyproject.toml").read_text(encoding="utf-8") + pypdf_lines = [line for line in pyproject.splitlines() if "pypdf" in line.lower()] + assert pypdf_lines, "pypdf dependency not found in pyproject.toml" + for line in pypdf_lines: + match = re.search(r'pypdf[>=<]+([0-9.]+)', line) + if match: + version = match.group(1) + major, minor = map(int, version.split('.')[:2]) + assert (major, minor) >= (6, 15), f"pypdf {version} < 6.15.0 in: {line}" diff --git a/tests/test_railway_runtime.py b/tests/test_railway_runtime.py index c773e94f..3c97e6d9 100644 --- a/tests/test_railway_runtime.py +++ b/tests/test_railway_runtime.py @@ -75,7 +75,8 @@ def test_railway_image_is_cpu_only_and_installs_only_its_runtime_surface(): def test_ci_audits_the_stripped_image_without_mutating_it(): workflow = _text(".github/workflows/ci.yml") - assert 'docker cp "$container":/usr/local/lib/python3.11/site-packages/.' in workflow + assert "sysconfig.get_path('purelib')" in workflow or 'sysconfig.get_path("purelib")' in workflow + assert 'docker cp "$container:$site_packages/."' in workflow assert 'python -m pip_audit --path "$audit_dir"' in workflow assert 'python -m pip install --disable-pip-version-check --no-cache-dir' in workflow assert 'pip-audit==2.10.1' in workflow diff --git a/tests/test_release_evidence.py b/tests/test_release_evidence.py index c30a0361..c283b98f 100644 --- a/tests/test_release_evidence.py +++ b/tests/test_release_evidence.py @@ -135,10 +135,7 @@ def _release_inputs(root, dist): builders = [ { "name": name, - "image": ( - "python:3.11-slim@sha256:" - "90744cff8f32887f075c47d747a173ff333e9e98801667af93c357fa9f5e28ff" - ), + "image": "github-hosted:ubuntu-latest/python-3.11", "python": "3.11", "environment_lock_sha256": "e" * 64, "toolchain": { @@ -515,7 +512,7 @@ def test_release_workflow_publishes_complete_captured_evidence(): assert "name: build-environment-evidence" in build assert "dist-repeat" not in build assert "Independent distribution builder ${{ matrix.builder }}" in reproducibility - assert "python:3.11-slim@sha256:90744cff" in reproducibility + assert "github-hosted:ubuntu-latest/python-3.11" in reproducibility assert 'builder: ["a", "b"]' in reproducibility assert "Compare independent distribution builders" in reproducibility assert "name: independent-reproducibility" in reproducibility diff --git a/tests/test_release_infrastructure.py b/tests/test_release_infrastructure.py index 96358533..7678d0bf 100644 --- a/tests/test_release_infrastructure.py +++ b/tests/test_release_infrastructure.py @@ -192,12 +192,14 @@ def test_ci_and_release_audit_production_image_dependencies(): assert "docker compose config --quiet" in ci assert "docker run --rm --entrypoint sh engraphis:ci" in ci assert 'python -m pip_audit --path "$audit_dir"' in ci - assert 'docker cp "$container":/usr/local/lib/python3.11/site-packages/.' in ci + assert 'docker cp "$container:$site_packages/."' in ci + legacy_audit_path = 'docker cp "$container":/usr/local/lib/python3.11/site-packages/.' + assert legacy_audit_path not in ci assert "tesseract-ocr" in _text("Dockerfile") assert ( _text("Dockerfile").splitlines()[1] == "FROM python:3.11-slim@sha256:" - "90744cff8f32887f075c47d747a173ff333e9e98801667af93c357fa9f5e28ff AS base" + "a630a63cdb314e2d138a2fca3e375e319e8568346ffafac5b980f888630ac4f1 AS base" ) assert "Verify production image OCR runtime" in ci assert "Verify production image OCR runtime" in release @@ -225,7 +227,8 @@ def test_ci_and_release_audit_production_image_dependencies(): assert "Audit production image dependencies" in release_docker assert "pip-audit==2.10.1" in release_docker assert 'docker create --name "$container" engraphis:release' in release_docker - assert 'docker cp "$container":/usr/local/lib/python3.11/site-packages/.' in release_docker + assert 'docker cp "$container:$site_packages/."' in release_docker + assert legacy_audit_path not in release_docker assert 'python -m pip_audit --path "$audit_dir"' in release_docker assert "reproducibility-check" in release_evidence.split("needs:", 1)[1].splitlines()[0] assert "installed-artifact-platform-smoke" in ( @@ -297,7 +300,7 @@ def test_release_builds_one_portable_open_core_wheel(): assert release.count("python -m build") == 2 assert "python -m build --outdir dist-repeat" not in release assert 'builder: ["a", "b"]' in release - assert "python:3.11-slim@sha256:90744cff" in release + assert "github-hosted:ubuntu-latest/python-3.11" in release assert "Compare independent distribution builders" in release assert "python scripts/verify_distribution_contents.py dist/*" in release assert "Build compiled wheels" not in release