diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0295e83..3902eb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,26 @@ All notable changes to Engraphis are documented here. Format loosely follows
## [Unreleased]
+### Removed
+
+- **"Signed compliance export" is no longer advertised on any surface.** It was granted to
+ Pro and Team by the control plane's plan→feature table and promoted in the account portal,
+ the dashboard upgrade panel, this changelog, and the README pricing table — but it was
+ never implemented on either side: no signing code in this client, and no export route, no
+ `export:*` token scope, and no export job kind in Engraphis Cloud. The `export` key is
+ removed from the plan→feature tables in both repos and from the dashboard's entitlement
+ vocabulary.
+ - **No entitlement behaviour changes.** Those tables are read only to decide which lock
+ badges and plan bullets to draw; every paid operation was, and still is, authorized by
+ the cloud's token scopes and paid-entitlement dependencies.
+ - **Local workspace export is unaffected and always was free.** `GET /export` returns the
+ full bi-temporal dump (memories, sessions, audit) on every plan and is deliberately not
+ entitlement-gated so data portability survives recovery mode. It is now listed in the
+ README as the free capability it is.
+ - `GET /export?signed=true` and `GET /analytics/export` still answer `501`, but now say
+ the capability is not implemented and name the working alternative, instead of implying
+ it is available in Engraphis Cloud.
+
### Changed
- Managed compute consent now travels with the cloud account: an installation connected to
@@ -797,7 +817,7 @@ and safe hosted deployment.
- Sleep-time consolidation with compaction accounting
- Personalized PageRank graph arm (HippoRAG-style)
- Offline signed license keys (no phone-home)
-- Pro analytics dashboard and compliance export
+- Pro analytics dashboard
- Code-symbol graph via tree-sitter or regex fallback
- Docker + docker-compose deployment
- 300+ tests, eval harness, ablation suite
diff --git a/README.md b/README.md
index 152330f..7a2cd22 100644
--- a/README.md
+++ b/README.md
@@ -419,11 +419,11 @@ recovery boundaries.
| Memory engine + 29 MCP tools | ✓ | ✓ | ✓ |
| Version-chain diffs, offline knowledge graph | ✓ | ✓ | ✓ |
| Manual local consolidation (dry-run by default) | ✓ | ✓ | ✓ |
+| Local workspace export (JSON: memories, sessions, audit) | ✓ | ✓ | ✓ |
| Hosted Cloud Sync | | ✓ | ✓ |
| Hosted Analytics | | ✓ | ✓ |
| Hosted Auto Consolidation + retention policy | | ✓ | ✓ |
| Hosted Auto Dreaming + managed proposals | | ✓ | ✓ |
-| Signed compliance export (checksummed bi-temporal bundle) | | ✓ | ✓ |
| Priority support | | ✓ | ✓ |
| Hosted multi-user dashboard: invitations, logins, roles, seat management | | | ✓ |
| Hosted Team audit log + CSV export | | | ✓ |
diff --git a/engraphis/hosted_client.py b/engraphis/hosted_client.py
index 560ff47..f889efe 100644
--- a/engraphis/hosted_client.py
+++ b/engraphis/hosted_client.py
@@ -39,7 +39,6 @@
"automation": "pro",
"consolidation": "pro",
"dreaming": "pro",
- "export": "pro",
"sync": "pro",
"team": "team",
}
diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py
index baf2b0c..cee65ef 100644
--- a/engraphis/routes/v2_api.py
+++ b/engraphis/routes/v2_api.py
@@ -1287,11 +1287,20 @@ def analytics(workspace: Optional[str] = None):
@router.get("/analytics/export")
def analytics_export(workspace: Optional[str] = None):
- """Self-contained HTML analytics report (inline CSS, zero CDN) — a shareable,
- archivable artifact. Same Pro gate as the analytics view it renders."""
+ """Not implemented here. A self-contained HTML analytics *report artifact* was planned
+ for this route and never built.
+
+ The previous 501 told customers to "download analytics reports from the Engraphis Cloud
+ dashboard", which overstates what is reachable: the control plane serves analytics as
+ JSON (``GET /analytics/latest``), and this client already surfaces exactly that through
+ ``GET /analytics``. Point callers at the data that exists rather than at a file that may
+ not. Kept as an explicit 501 rather than removed so an older dashboard build calling this
+ path gets a truthful answer instead of a 404."""
raise HTTPException(status_code=501, detail={
- "error": "Download analytics reports from the Engraphis Cloud dashboard.",
- "managed_cloud": True,
+ "error": "This build does not generate analytics report files. Use GET /analytics "
+ "for the same data as JSON.",
+ "implemented": False,
+ "alternative": "/analytics",
})
@@ -1314,19 +1323,26 @@ def ready():
status_code=200 if is_ready else 503)
-# ── compliance export (Pro) ───────────────────────────────────────────────────
+# ── workspace export (local, free) ────────────────────────────────────────────
@router.get("/export")
def export(workspace: Optional[str] = None, signed: bool = False):
- """Full bi-temporal workspace dump (memories + sessions + audit). Pro-gated.
+ """Full bi-temporal workspace dump (memories + sessions + audit).
+
+ Local and free: this is the data-portability path that must keep working even in
+ recovery mode, so it is deliberately not entitlement-gated.
- ``signed=true`` wraps the dump in a SHA-256 compliance manifest (see
- :func:`_sign_export`) — a tamper-evident, self-verifying audit bundle."""
+ ``signed=true`` was specified as a SHA-256 compliance manifest wrapping the same dump —
+ a tamper-evident, self-verifying audit bundle. It was never implemented; no signing
+ code exists in this client and Engraphis Cloud has no export route, no ``export:*``
+ token scope, and no export job kind. It answers 501 rather than silently returning an
+ *unsigned* bundle, because a caller asking for tamper-evidence must not be handed
+ something that merely looks like it."""
if signed:
raise HTTPException(status_code=501, detail={
- "error": "Signed compliance exports are available in Engraphis Cloud.",
- "cloud_only": True,
- "feature": "export",
- "upgrade_url": licensing.upgrade_url(),
+ "error": "Signed compliance exports are not implemented. Omit signed=true for "
+ "the unsigned workspace export, which contains the same data.",
+ "implemented": False,
+ "alternative": "/export",
})
ws = workspace or _default_ws()
return _run(service().export_workspace, workspace=ws, recovery=True)
@@ -1730,12 +1746,17 @@ def code_export(workspace: str, repo: str):
# and only ``pro``/``team`` are paid; any other value — unknown, empty, or mis-cased —
# resolves to no features, exactly as the server treats it.
#
-# The server's own keys are {analytics, automation, export, sync, team}. This client's
+# The server's own keys are {analytics, automation, sync, team}. This client's
# commercial manifest additionally names Auto Consolidation and Auto Dreaming, which the
# server grants under ``automation``. They are expanded here so the dashboard can never
# draw a lock on a capability the customer's plan already includes.
+#
+# ``export`` was removed from both tables: it was disclosed by the server and rendered here
+# for the whole pre-launch period while no signed-export capability existed on either side.
+# Plain local workspace export is unaffected — it is a free, local-only route (``GET
+# /export``) and was never a hosted entitlement.
_AUTOMATION_FEATURES = ("automation", "consolidation", "dreaming")
-_PRO_FEATURES = ("analytics", "export", "sync") + _AUTOMATION_FEATURES
+_PRO_FEATURES = ("analytics", "sync") + _AUTOMATION_FEATURES
_PLAN_FEATURES = {
"free": (),
"local": (),
@@ -1751,7 +1772,6 @@ def code_export(workspace: str, repo: str):
"automation": "Automation",
"consolidation": "Auto Consolidation",
"dreaming": "Auto Dreaming",
- "export": "Compliance export",
"sync": "Cloud Sync",
"team": "Team administration",
}
diff --git a/engraphis/static/dashboard.js b/engraphis/static/dashboard.js
index 902f0e0..d9da2e8 100644
--- a/engraphis/static/dashboard.js
+++ b/engraphis/static/dashboard.js
@@ -159,7 +159,7 @@ async function loadOverviewAnalytics(){
/* ── shared hosted upgrade / trial CTA ── */
function hostedPlanUrl(plan,trial){const raw=(LIC&&(plan==='team'?LIC.team_upgrade_url:LIC.pro_upgrade_url))||(LIC&&LIC.upgrade_url);const safe=safeUrl(raw);if(!safe||safe==='#')return '#';try{const url=new URL(safe,location.href);if(plan==='pro'||plan==='team')url.searchParams.set('plan',plan);if(trial)url.searchParams.set('trial',plan);return url.href}catch(e){return safe}}
-function unlockHtml(feature,plan){const url=hostedPlanUrl(plan),trialUrl=hostedPlanUrl(plan,true),team=plan==='team';const used=LIC&&LIC.trial&&LIC.trial.used;const trial=team?'Start hosted Team trial':'Start hosted Pro trial';const purchase=team?'Purchase Team license':'Purchase Pro license';const price=team?'$20 per seat/month or $200 per seat/year':'$10/month or $100/year';const detail=used?'Your free trial has already been used.':`The email-confirmed, no-card trial lasts exactly ${TRIAL_DAYS} active days.`;const benefits=team?['Everything in Pro','Hosted organizations, invitations, and named seats','Roles, scoped credentials, and Team audit history']:['Hosted Cloud Sync across your installations','Growth, retention, decay, and entity Analytics','Auto Consolidation with hosted retention policies','Auto Dreaming with reviewable managed proposals','Signed compliance exports with bi-temporal checksums','Priority support'];return ` Make the local memory engine work across your installations—and keep improving without manual upkeep. ${detail} Make the local memory engine work across your installations—and keep improving without manual upkeep. ${detail}Unlock ${esc(feature)} and more
${benefits.map(item=>`
Unlock ${esc(feature)} and more
${benefits.map(item=>`