From 19520d153701d6a26e41819747ed22d37d7d1fe6 Mon Sep 17 00:00:00 2001 From: Jaixii Date: Sun, 26 Jul 2026 13:49:54 -0400 Subject: [PATCH] fix(export): stop advertising a signed compliance export that does not exist The dashboard's Pro upgrade panel sold "Signed compliance exports with bi-temporal checksums" and the README pricing table sold "Signed compliance export (checksummed bi-temporal bundle)", but the capability was never built: * no signing code in this client -- `/export`'s docstring referenced `:func:`_sign_export``, which does not exist anywhere in the repo; * `GET /export?signed=true` answered 501 "available in Engraphis Cloud", and `GET /analytics/export` answered 501 "download from the Engraphis Cloud dashboard"; * but Engraphis Cloud has no export route, no `export:*` token scope (`_token_scopes`, api/control.py:567-599), and no export job kind (`Literal["analytics", "consolidate", "dream"]`). Both ends pointed at each other and neither implemented it. Changes: * drop `export` from `_PRO_FEATURES`, `_FEATURE_LABELS`, and `_REQUIRED_PLAN`, mirroring the server's PLAN_FEATURES change; * make both 501s truthful -- they now say the capability is not implemented and name the working alternative instead of implying it exists in Cloud; * remove the benefit line from the upgrade panel and the README pricing row; * `exportWorkspace()` drops its unreachable `signed` argument and its false "Export is a Pro feature" 402 toast. Plain local workspace export is unaffected and always was free: `GET /export` returns the full bi-temporal dump 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. No enforcement behaviour changes. These tables are presentation only (see `entitled_features`' docstring); `HostedFeatureError`/`LicenseError` is never raised anywhere in the client, so `required_plan` fed nothing but a dormant handler. Updates all six hand-copied mirrors of the server's plan->feature table: test_client_launch, test_licensing_launch, test_hosted_plan_resolution, test_cloud_session, tests/e2e/commercial.spec.js, and test_dashboard_auth_placement. There is still no automated cross-repo check. Companion server change: engraphis-cloud PR "fix(entitlements): stop selling "export", which is implemented nowhere". Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 22 +++++++++++- README.md | 2 +- engraphis/hosted_client.py | 1 - engraphis/routes/v2_api.py | 50 ++++++++++++++++++-------- engraphis/static/dashboard.js | 4 +-- tests/e2e/commercial.spec.js | 3 +- tests/test_client_launch.py | 4 +-- tests/test_cloud_session.py | 2 +- tests/test_dashboard_auth_placement.py | 5 ++- tests/test_dashboard_v2.py | 16 +++++++-- tests/test_hosted_plan_resolution.py | 4 +-- tests/test_licensing_launch.py | 4 +-- 12 files changed, 85 insertions(+), 32 deletions(-) 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 `
ENGRAPHIS ${team?'TEAM':'PRO'}

Unlock ${esc(feature)} and more

Make the local memory engine work across your installations—and keep improving without manual upkeep.

${price}
Your license unlocks
    ${benefits.map(item=>`
  • ${esc(item)}
  • `).join('')}

${detail}

${used?'':`${trial}`}${purchase}
`} +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','Priority support'];return `
ENGRAPHIS ${team?'TEAM':'PRO'}

Unlock ${esc(feature)} and more

Make the local memory engine work across your installations—and keep improving without manual upkeep.

${price}
Your license unlocks
    ${benefits.map(item=>`
  • ${esc(item)}
  • `).join('')}

${detail}

${used?'':`${trial}`}${purchase}
`} function startTrialPlan(plan){const url=hostedPlanUrl(plan,true);if(url==='#'){toast('Hosted signup URL is not configured','err');return}const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener';link.click()} function startTrial(){return startTrialPlan('pro')} function startTeamTrial(){return startTrialPlan('team')} @@ -447,7 +447,7 @@ function renderLicense(d){ :`
`; el.innerHTML=h; } -async function exportWorkspace(signed){try{const d=await api('/export?workspace='+encodeURIComponent(WS||'')+(signed?'&signed=1':''));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-export-'+(signed?'signed-':'')+Date.now()+'.json';a.click();URL.revokeObjectURL(a.href);toast(signed?'Signed compliance export downloaded':'Exported','ok')}catch(e){toast(e.status===402?'Export is a Pro feature — start your free trial':e.message,'err')}} +async function exportWorkspace(){try{const d=await api('/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-export-'+Date.now()+'.json';a.click();URL.revokeObjectURL(a.href);toast('Exported','ok')}catch(e){toast(e.message,'err')}} /* Hosted Team is a service CTA; local identity and seat administration are not shipped. */ async function loadTeam(){const el=document.getElementById('team-body');let url=hostedPlanUrl('team');try{const st=await api('/auth/state');if(url==='#'&&st&&st.cloud_url)url=safeUrl(st.cloud_url)}catch(e){}let trialUrl=url;if(url!=='#')try{const parsed=new URL(url,location.href);parsed.searchParams.set('trial','team');trialUrl=parsed.href}catch(e){}el.innerHTML=`
Engraphis Team Cloud HOSTED
Organizations, invitations, roles, named seats, scoped device credentials, and team audit run on the private hosted service. This local dashboard is intentionally single-user.
The email-confirmed trial lasts exactly ${TRIAL_DAYS} active days. A separate local-only write grace is capped at 24 hours and never extends Team or other cloud access.
Start hosted Team trialOpen Team Cloud
`} diff --git a/tests/e2e/commercial.spec.js b/tests/e2e/commercial.spec.js index c6607e4..b704b30 100644 --- a/tests/e2e/commercial.spec.js +++ b/tests/e2e/commercial.spec.js @@ -9,12 +9,11 @@ const knownFeatures = { automation: 'Automation', consolidation: 'Auto Consolidation', dreaming: 'Auto Dreaming', - export: 'Compliance export', sync: 'Cloud Sync', team: 'Team administration', }; -const proFeatures = ['analytics', 'automation', 'consolidation', 'dreaming', 'export', 'sync']; +const proFeatures = ['analytics', 'automation', 'consolidation', 'dreaming', 'sync']; const teamFeatures = [...proFeatures, 'team']; function licenseFor(plan, features) { diff --git a/tests/test_client_launch.py b/tests/test_client_launch.py index b8ae237..b65d4bf 100644 --- a/tests/test_client_launch.py +++ b/tests/test_client_launch.py @@ -45,8 +45,8 @@ # purchased capability silently renders as locked. SERVER_PLAN_FEATURES = { "free": set(), - "pro": {"analytics", "automation", "export", "sync"}, - "team": {"analytics", "automation", "export", "sync", "team"}, + "pro": {"analytics", "automation", "sync"}, + "team": {"analytics", "automation", "sync", "team"}, } # Named separately by this client's commercial manifest; the server grants both under # ``automation``, so any plan granting ``automation`` must grant these too. diff --git a/tests/test_cloud_session.py b/tests/test_cloud_session.py index c5334e3..f5ac02e 100644 --- a/tests/test_cloud_session.py +++ b/tests/test_cloud_session.py @@ -311,7 +311,7 @@ def _downgrade_state(monkeypatch, *, body: dict) -> dict: "token_subject": "member", "plan": "team", "cloud_access_active": True, - "cloud_features": ["analytics", "automation", "export", "sync", "team"], + "cloud_features": ["analytics", "automation", "sync", "team"], } def _replace(value: dict) -> None: diff --git a/tests/test_dashboard_auth_placement.py b/tests/test_dashboard_auth_placement.py index da32b1d..fbcf0d6 100644 --- a/tests/test_dashboard_auth_placement.py +++ b/tests/test_dashboard_auth_placement.py @@ -330,11 +330,14 @@ def test_pro_upgrade_panel_lists_every_pro_benefit_and_purchase_cta(): "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", ): assert benefit in script assert ".upgrade-panel" in styles + # Regression: the panel sold "Signed compliance exports with bi-temporal checksums" + # while no signing code existed in this client and Engraphis Cloud had no export + # route, scope, or job kind. Do not re-add it without a capability behind it. + assert "compliance export" not in script.lower() def test_team_invitations_and_password_setup_are_not_in_local_client(): diff --git a/tests/test_dashboard_v2.py b/tests/test_dashboard_v2.py index 7ad408f..4494e59 100644 --- a/tests/test_dashboard_v2.py +++ b/tests/test_dashboard_v2.py @@ -282,16 +282,28 @@ def test_portfolio_and_report_analytics_are_hosted_only(monkeypatch, tmp_path): assert client.get("/api/analytics/export?workspace=demo").status_code == 501 -def test_raw_owner_export_is_free_but_signed_report_is_cloud_only( +def test_raw_owner_export_is_free_and_signed_export_is_honestly_unimplemented( monkeypatch, tmp_path ): + """The signed variant must not claim to exist somewhere else. + + It previously answered ``cloud_only: True`` — but Engraphis Cloud has no export route, + no ``export:*`` token scope, and no export job kind, so that pointed a customer at a + product that does not exist. The 501 now says the capability is unimplemented and names + the working unsigned export instead. + """ + with _client(monkeypatch, tmp_path) as client: raw = client.get("/api/export?workspace=demo") assert raw.status_code == 200 assert raw.json()["counts"]["memories"] >= 1 signed = client.get("/api/export?workspace=demo&signed=true") assert signed.status_code == 501 - assert signed.json()["detail"]["cloud_only"] is True + detail = signed.json()["detail"] + assert detail["implemented"] is False + assert detail["alternative"] == "/export" + assert "cloud_only" not in detail + assert "Engraphis Cloud" not in detail["error"] def test_health_and_readiness_remain_public(monkeypatch, tmp_path): diff --git a/tests/test_hosted_plan_resolution.py b/tests/test_hosted_plan_resolution.py index 8dbedf3..b3812c1 100644 --- a/tests/test_hosted_plan_resolution.py +++ b/tests/test_hosted_plan_resolution.py @@ -53,8 +53,8 @@ # engraphis-cloud/engraphis_cloud/entitlements.py ``EntitlementDTO`` / ``PLAN_FEATURES``. SERVER_PLAN_FEATURES = { "free": [], - "pro": ["analytics", "automation", "export", "sync"], - "team": ["analytics", "automation", "export", "sync", "team"], + "pro": ["analytics", "automation", "sync"], + "team": ["analytics", "automation", "sync", "team"], } diff --git a/tests/test_licensing_launch.py b/tests/test_licensing_launch.py index a129618..9f07b57 100644 --- a/tests/test_licensing_launch.py +++ b/tests/test_licensing_launch.py @@ -28,8 +28,8 @@ SERVER_PAID_PLANS = {"pro", "team"} SERVER_PLAN_FEATURES = { "free": set(), - "pro": {"analytics", "automation", "export", "sync"}, - "team": {"analytics", "automation", "export", "sync", "team"}, + "pro": {"analytics", "automation", "sync"}, + "team": {"analytics", "automation", "sync", "team"}, } SERVER_TRIAL_DURATION_SECONDS = 259_200 SERVER_WORKSPACE_WRITE_GRACE_MAX_SECONDS = 86_400