From e3fe53e317e151a7dd192210e9f3b086d2877912 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Thu, 23 Jul 2026 14:20:27 -0400 Subject: [PATCH 1/2] release: prepare 1.0.1 client reliability fixes --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .claude-plugin/skill-assets.sha256 | 4 +- .env.example | 5 -- .github/dependabot.yml | 4 +- .github/workflows/codeql.yml | 31 +++++++++++ AGENTS.md | 2 +- CHANGELOG.md | 17 +++++- README.md | 9 ++- SECURITY.md | 6 +- deploy/railway-template.json | 4 -- docs/HOSTING_RAILWAY.md | 5 +- docs/RAILWAY_TEMPLATE.md | 4 +- docs/SYNC.md | 2 +- engraphis/__init__.py | 2 +- engraphis/backends/sync_relay.py | 2 +- engraphis/cloud_features.py | 30 ++++------ engraphis/commercial_manifest.json | 2 +- engraphis/config.py | 7 ++- engraphis/routes/v2_api.py | 24 +++++--- engraphis/static/dashboard.js | 9 ++- pyproject.toml | 2 +- scripts/sync.py | 2 +- tests/test_cloud_features.py | 11 ++-- tests/test_config.py | 15 ++++- tests/test_dashboard_dream_ui.py | 2 +- tests/test_dashboard_v2.py | 83 ++++++++++++++++++++++++++++ tests/test_release_infrastructure.py | 2 + tests/test_sync_dashboard.py | 6 ++ 29 files changed, 215 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/codeql.yml diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 34003afe..66251cb5 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.0.0" + "version": "1.0.1" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3ac212b4..ed29620b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engraphis-memory", - "version": "1.0.0", + "version": "1.0.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 87ca0b2c..5cbcced7 100644 --- a/.claude-plugin/skill-assets.sha256 +++ b/.claude-plugin/skill-assets.sha256 @@ -1,5 +1,5 @@ -f4cb86b658832adc9c2d71b449091fbfcb327570cce207ab071d500029c79ac0 .claude-plugin/marketplace.json -9d5e58b9e2692e30d29ccbadbf865b30a6db72f3ff209694518c848a2b1b1e51 .claude-plugin/plugin.json +5c5336f5cc0e46c118dd13aace779555c07df1d5ec89828c02b1763a39bb637b .claude-plugin/marketplace.json +5dcc1fed4a7f2a527b1f756b90dc12d9b80fe393df0090e22a603f71c1b4faba .claude-plugin/plugin.json 89bf2728e44a3c877e31982d387fcfab10fbe065eb6441b2792375ca5105c07c skills/engraphis-memory/SKILL.md a295b0448e2ff372ddd8ea4e0bc8dc53f3d4bd56ff1fbd0d88cf4b6179511ce1 skills/engraphis-memory/references/CONVENTIONS.md 45f4b4ad9dbfd39f2b377083d9b3eec5eed7cba7cb8fa139e3f420bdd6105343 skills/engraphis-memory/references/SCOPING.md diff --git a/.env.example b/.env.example index ecb3263f..9b96ac55 100644 --- a/.env.example +++ b/.env.example @@ -151,11 +151,6 @@ ENGRAPHIS_LLM_API_KEY=sk-your-key-here # ENGRAPHIS_CLOUD_TOKEN_SUBJECT=member # ENGRAPHIS_CLOUD_ACCESS_TOKEN= -# Managed compute is explicit opt-in. When enabled, the client sends a bounded workspace -# snapshot capped at 16 MiB; secret-class and session-scoped rows are excluded before -# serialization, and secret-class rows are rejected by the service. -ENGRAPHIS_MANAGED_COMPUTE_CONSENT=0 - # Persistent customer state (cloud session and optional saved scoped relay bearer). # Locally defaults to ~/.engraphis; in a container use a private persistent volume. # ENGRAPHIS_STATE_DIR=/data/.engraphis diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3637cbb5..d51b9329 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,11 +4,11 @@ updates: directory: "/" schedule: interval: "weekly" - open-pull-requests-limit: 10 + open-pull-requests-limit: 5 labels: - "dependencies" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - open-pull-requests-limit: 5 + open-pull-requests-limit: 3 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..76a1d0ce --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,31 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "23 4 * * 1" + +permissions: + contents: read + security-events: write + +jobs: + analyze: + name: Analyze ${{ matrix.language }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: ["python", "javascript-typescript"] + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - name: Initialize CodeQL + uses: github/codeql-action/init@3b0bd1d116c0bde30213346b22d4f634d96a2fb0 # v3 + with: + languages: ${{ matrix.language }} + build-mode: none + - name: Analyze + uses: github/codeql-action/analyze@3b0bd1d116c0bde30213346b22d4f634d96a2fb0 # v3 diff --git a/AGENTS.md b/AGENTS.md index 85d9be9e..7e42ebbf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,7 +68,7 @@ python -m scripts.consolidate --db engraphis.db --workspace acme --dry-run # ── Sync (local shared-folder transport or hosted Cloud Sync — see docs/SYNC.md) ── python -m scripts.sync --db engraphis.db --workspace acme --remote ~/Dropbox/engraphis --dry-run -python -m scripts.sync --db engraphis.db --workspace acme --relay https://team.engraphis.com # or bare --relay + ENGRAPHIS_RELAY_URL +python -m scripts.sync --db engraphis.db --workspace acme --relay https://relay.engraphis.com # or bare --relay + ENGRAPHIS_RELAY_URL # ── Run the v1 server (needs the full install) ─────────────────────────────── python -m scripts.start_server # http://127.0.0.1:8700 (dashboard at /, schema at /openapi.json) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d314e64..af9d94d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,19 @@ All notable changes to Engraphis are documented here. Format loosely follows - Graph GET requests are strictly read-only. While an explicit mutating index job runs, graph reads return a rebuilding conflict instead of mixing old and partially derived metrics. +## [1.0.1] - 2026-07-23 + +Public 1.0.1 client reliability release. + +### Fixed + +- Cloud Sync now defaults to `https://relay.engraphis.com` and safely migrates the former + dashboard host and retired Railway relay URL without changing customer-provided relay URLs. +- Hosted Automation and maintenance requests now use the selected workspace end to end rather + than silently falling back to the first workspace. +- The Automation tab has one proposal action, clear managed-upload disclosure, and automatic + entitlement-gated managed-compute availability while preserving snapshot redaction and limits. + ## [1.0.0] - 2026-07-23 Public 1.0.0 open-core GA release. @@ -99,8 +112,8 @@ Public 1.0.0 open-core GA release. implementations from the public source and package artifacts. - Bare memory IDs, shared-workspace controls, graph entities, statistics, snapshots, exports, audit rows, and keyword fallbacks cannot cross authenticated session or workspace boundaries. -- Managed uploads require explicit consent, are capped at 16 MiB and 100,000 rows, omit all - session-scoped and secret-class memories, and surface only fixed client-safe provider errors. +- Managed uploads are capped at 16 MiB and 100,000 rows, omit all session-scoped and + secret-class memories, and surface only fixed client-safe provider errors. - Customer credentials remain owner-only, redirect-safe, serialized during rotation, and are never substituted with an unproven local machine identifier. diff --git a/README.md b/README.md index 40f34d5e..2d0b6e96 100644 --- a/README.md +++ b/README.md @@ -601,15 +601,15 @@ Manual consolidation is free and remains local. Use the dashboard's **Consolidat `python -m scripts.consolidate`. Dry-run is the default. Pro and Team add **hosted** Auto Consolidation and Auto Dreaming. The public Automation tab is a -policy/status client: it submits an explicitly consented, bounded snapshot to private managed -compute and displays reviewable jobs or proposals. The scheduling, analytics, dreaming, and +policy/status client: it submits a bounded snapshot to private managed compute and displays +reviewable jobs or proposals. The scheduling, analytics, dreaming, and consolidation automation algorithms run in Engraphis Cloud; this repository ships no premium background loop, cron wrapper, or worker. Secret-class and session-scoped memories are excluded before a managed snapshot is serialized; secret-class rows are rejected again by the hosted service. The encoded payload is capped at -16 MiB. Set `ENGRAPHIS_MANAGED_COMPUTE_CONSENT=1` only after reviewing that boundary. A managed -proposal does not silently rewrite the local database. +16 MiB. Managed compute is enabled automatically for authorized customers. A managed proposal +does not silently rewrite the local database. Manual consolidation can also use schema-validated LLM output through `MemoryService.consolidate`, `POST /api/consolidate`, `engraphis_consolidate`, or @@ -656,7 +656,6 @@ All via environment (or `.env`): | `ENGRAPHIS_CLOUD_REFRESH_CREDENTIAL` | — | Bootstrap-only rotating hosted credential; after first use the owner-only cloud session replacement takes precedence | | `ENGRAPHIS_CLOUD_TOKEN_SUBJECT` | `member` | Subject fixed during hosted bootstrap (`device` or `member`); set explicitly with an environment-only refresh credential | | `ENGRAPHIS_CLOUD_ACCESS_TOKEN` | — | Optional short-lived access token for ephemeral jobs | -| `ENGRAPHIS_MANAGED_COMPUTE_CONSENT` | `0` | Explicit opt-in required before uploading a bounded snapshot for hosted Analytics/Automation | See `.env.example` for the full customer-runtime and managed-service client options. diff --git a/SECURITY.md b/SECURITY.md index e55a34fd..028d384f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -105,9 +105,9 @@ them back as `expected_head` / `expected_count` when independent evidence is req no paid-key parser, signer, issuer, local feature gate, or long-lived-key relay exchange. - **Server authority:** every hosted and cost-bearing operation is authorized by the private control plane; local plan labels and upgrade URLs are presentation metadata only. -- **Managed-compute consent:** Analytics, Auto Dreaming, and Auto Consolidation upload a bounded - snapshot only after `ENGRAPHIS_MANAGED_COMPUTE_CONSENT=1`. Secret-class memories are excluded - before serialization and rejected again by the hosted service. +- **Managed-compute boundaries:** Analytics, Auto Dreaming, and Auto Consolidation upload a + bounded snapshot for authorized customers. Secret-class memories are excluded before + serialization and rejected again by the hosted service. - **Trial and grace are separate:** an email-confirmed trial lasts exactly 3 active days. A separately bounded, maximum-24-hour local workspace-write grace never extends the trial, subscription, Cloud Sync, managed compute, Team access, seats, or credentials. diff --git a/deploy/railway-template.json b/deploy/railway-template.json index 734c9aac..e751c40e 100644 --- a/deploy/railway-template.json +++ b/deploy/railway-template.json @@ -48,10 +48,6 @@ "ENGRAPHIS_CLOUD_COMPUTE_URL": { "value": "https://compute.engraphis.com", "required": false - }, - "ENGRAPHIS_MANAGED_COMPUTE_CONSENT": { - "value": "0", - "required": true } } } diff --git a/docs/HOSTING_RAILWAY.md b/docs/HOSTING_RAILWAY.md index 1e3b1641..afd094fe 100644 --- a/docs/HOSTING_RAILWAY.md +++ b/docs/HOSTING_RAILWAY.md @@ -35,14 +35,13 @@ ENGRAPHIS_CLOUD_CONTROL_URL=https://api.engraphis.com ENGRAPHIS_CLOUD_COMPUTE_URL=https://compute.engraphis.com ENGRAPHIS_CLOUD_ORGANIZATION_ID=org_replace_me ENGRAPHIS_CLOUD_REFRESH_CREDENTIAL= -ENGRAPHIS_MANAGED_COMPUTE_CONSENT=0 ``` Prefer mounting the owner-only cloud session file rather than placing a rotating refresh credential directly in deployment configuration. An injected environment credential is only the bootstrap value; after rotation, the owner-only saved replacement takes precedence. Enabling -managed-compute consent may upload a snapshot capped at 16 MiB; secret-class and session-scoped -rows are excluded client-side, and secret-class rows are rejected server-side. +managed compute for an authorized customer may upload a snapshot capped at 16 MiB; secret-class +and session-scoped rows are excluded client-side, and secret-class rows are rejected server-side. ## Persistence and recovery diff --git a/docs/RAILWAY_TEMPLATE.md b/docs/RAILWAY_TEMPLATE.md index 5dcbc129..20dbe610 100644 --- a/docs/RAILWAY_TEMPLATE.md +++ b/docs/RAILWAY_TEMPLATE.md @@ -14,8 +14,8 @@ issuer, relay, managed compute, Auto Dreaming, Auto Consolidation, or Team ident - No vendor signer, billing, mail, Team-admin, relay-storage, or worker secrets. Hosted customer endpoint variables may be exposed as optional inputs, but a refresh credential -must be injected as a secret or mounted owner-only state file. Managed compute consent defaults -off. +must be injected as a secret or mounted owner-only state file. Managed compute is enabled for +authorized customers automatically. ## Publish gate diff --git a/docs/SYNC.md b/docs/SYNC.md index 18d78667..4d8ca74d 100644 --- a/docs/SYNC.md +++ b/docs/SYNC.md @@ -60,7 +60,7 @@ The one-shot customer client remains available for explicit sync operations: python -m scripts.sync \ --db engraphis.db \ --workspace acme \ - --relay https://team.engraphis.com + --relay https://relay.engraphis.com ``` The dashboard's **Sync now** action invokes the same customer protocol. The public package does diff --git a/engraphis/__init__.py b/engraphis/__init__.py index 93841fd1..2e9166c5 100644 --- a/engraphis/__init__.py +++ b/engraphis/__init__.py @@ -7,4 +7,4 @@ 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.0.0" + __version__ = "1.0.1" diff --git a/engraphis/backends/sync_relay.py b/engraphis/backends/sync_relay.py index bfb46e4c..1fdc05c4 100644 --- a/engraphis/backends/sync_relay.py +++ b/engraphis/backends/sync_relay.py @@ -309,7 +309,7 @@ def _safe_bundle_name(name: object) -> str: class RelayTransport: """A ``SyncTransport`` backed by the customer sync relay. - ``base_url`` is the relay root (e.g. ``https://team.engraphis.com``). ``workspace_id`` + ``base_url`` is the relay root (e.g. ``https://relay.engraphis.com``). ``workspace_id`` scopes bundles to one workspace. ``access_token`` must be a short-lived scoped cloud bearer. The legacy-named ``license_key`` parameter is accepted only as a call-site alias for that bearer; values with the retired ``ENGR1`` prefix are rejected. With no diff --git a/engraphis/cloud_features.py b/engraphis/cloud_features.py index 5002d45a..acda9fed 100644 --- a/engraphis/cloud_features.py +++ b/engraphis/cloud_features.py @@ -1,14 +1,13 @@ """Thin client protocol for private Engraphis managed-compute features. The open package deliberately contains no analytics, dreaming, or automatic-consolidation -algorithm. It can prepare an explicitly consented workspace snapshot, exclude secret-classified -memories, and send that snapshot to the separately operated Engraphis Cloud service. The service -is authoritative for entitlements and performs all paid computation. +algorithm. It prepares a bounded workspace snapshot, excludes secret-classified memories, and +sends that snapshot to the separately operated Engraphis Cloud service. The service is +authoritative for entitlements and performs all paid computation. """ from __future__ import annotations import json -import os import re import time import urllib.error @@ -42,14 +41,14 @@ def redirect_request(self, req, fp, code, msg, headers, newurl): return None -def _truthy(name: str) -> bool: - return os.environ.get(name, "").strip().casefold() in {"1", "true", "yes", "on"} - - def managed_compute_consent() -> bool: - """Return the explicit opt-in flag; an entitlement alone is never consent.""" + """Return whether managed compute is enabled for the customer client. - return _truthy("ENGRAPHIS_MANAGED_COMPUTE_CONSENT") + Managed compute is enabled automatically for every user with a valid cloud entitlement. + This compatibility helper intentionally no longer reads an environment variable. + """ + + return True def _public_http_error(status: int) -> tuple[str, bool]: @@ -184,17 +183,10 @@ def _build_managed_snapshot_locked(service: Any, workspace: str, *, generation: Optional[int] = None) -> tuple[str, dict]: """Build the bounded client-side transport document for one local workspace. - Secret-classified rows are omitted before serialization. Sensitive (but not secret) content - is included only after the same explicit managed-compute consent as normal content. + Secret-classified rows are omitted before serialization. The ``consent`` parameter remains + accepted for source compatibility, but managed compute is enabled automatically. """ - allowed = managed_compute_consent() if consent is None else bool(consent) - if not allowed: - raise CloudFeatureError( - "Managed compute is off. Opt in before uploading workspace content by setting " - "ENGRAPHIS_MANAGED_COMPUTE_CONSENT=1.", - status=409, - ) clean_workspace = service._clean_ws(workspace) workspace_id = service._lookup_workspace(clean_workspace) if not workspace_id: diff --git a/engraphis/commercial_manifest.json b/engraphis/commercial_manifest.json index 7b0e0b9b..f27f80ee 100644 --- a/engraphis/commercial_manifest.json +++ b/engraphis/commercial_manifest.json @@ -1,6 +1,6 @@ { "schema": "engraphis-commercial/v2", - "version": "1.0.0", + "version": "1.0.1", "control_plane": "https://api.engraphis.com", "managed_dashboard": "https://team.engraphis.com", "billing": { diff --git a/engraphis/config.py b/engraphis/config.py index bf0425d3..4578837d 100644 --- a/engraphis/config.py +++ b/engraphis/config.py @@ -421,9 +421,9 @@ def _configured_db_path(root: Path = _PROJECT_ROOT) -> str: return str(target) -#: Vendor-hosted managed sync service. Customer deployments normally override this with -#: their own dashboard URL; local Pro clients retain the managed default. -DEFAULT_RELAY_URL = "https://team.engraphis.com" +#: Vendor-hosted managed sync service. The account dashboard remains at +#: ``team.engraphis.com``; sync traffic goes to its separate relay endpoint. +DEFAULT_RELAY_URL = "https://relay.engraphis.com" SERVICE_MODES = ("customer",) # The public package is a customer data plane and contains no vendor authority or hosted @@ -435,6 +435,7 @@ def _configured_db_path(root: Path = _PROJECT_ROOT) -> str: # managed service. Arbitrary signed URLs remain authoritative. RETIRED_RELAY_URLS = frozenset({ "https://engraphis-production.up.railway.app", + "https://team.engraphis.com", }) def _env(key: str, default: str = "") -> str: diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py index 3933f97c..5b0aab62 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -116,8 +116,16 @@ def _default_ws() -> Optional[str]: return wss[0]["name"] if wss else None -def _require_ws() -> str: - """Like _default_ws but raises 400 if no workspace exists yet.""" +def _require_ws(workspace: Optional[str] = None) -> str: + """Resolve an explicitly selected workspace, with a legacy default fallback. + + Cloud automation is workspace-scoped. Existing clients that omit the query + parameter retain the historic first-workspace behavior, but an explicit value + is cleaned and authorized before it is used. Callers validate existence at + the cloud boundary so an unknown selection receives the documented 404. + """ + if workspace is not None: + return service()._clean_ws(workspace) ws = _default_ws() if not ws: raise HTTPException(status_code=400, detail={"error": "No workspace exists yet. Create one first."}) @@ -1248,11 +1256,11 @@ class _AutomationReq(BaseModel): @router.get("/automation") -def automation_get(): +def automation_get(workspace: Optional[str] = None): """Read the cloud-authoritative managed-maintenance policy.""" from engraphis.cloud_features import CloudFeatureClient - ws = _require_ws() + ws = _require_ws(workspace) workspace_id = service()._lookup_workspace(ws) if not workspace_id: raise HTTPException(status_code=404, detail={ @@ -1285,11 +1293,11 @@ def automation_get(): @router.post("/automation") -def automation_set(req: _AutomationReq): +def automation_set(req: _AutomationReq, workspace: Optional[str] = None): """Persist scheduling only in the private cloud data store.""" from engraphis.cloud_features import CloudFeatureClient, build_managed_snapshot - ws = _require_ws() + ws = _require_ws(workspace) workspace_id = service()._lookup_workspace(ws) if not workspace_id: raise HTTPException(status_code=404, detail={ @@ -1337,11 +1345,11 @@ class _MaintenanceReq(BaseModel): @router.post("/maintenance/run") -def maintenance_run(req: _MaintenanceReq): +def maintenance_run(req: _MaintenanceReq, workspace: Optional[str] = None): """Submit consolidation to managed compute; results remain generation-bound.""" from engraphis.cloud_features import run_managed_job - ws = _require_ws() + ws = _require_ws(workspace) envelope = _managed_call(run_managed_job, service(), ws, "consolidate") result = envelope.get("result", envelope) return { diff --git a/engraphis/static/dashboard.js b/engraphis/static/dashboard.js index 2e4aec2c..8de80a11 100644 --- a/engraphis/static/dashboard.js +++ b/engraphis/static/dashboard.js @@ -179,15 +179,15 @@ function renderAnalytics(a,isPortfolio){const t=a.totals||{},f=a.decay_forecast| async function loadAnalytics(){const el=document.getElementById('analytics-body'),lock=document.getElementById('an-lock'),acts=document.getElementById('an-actions');el.innerHTML='
';try{const a=await api('/analytics?workspace='+encodeURIComponent(WS||''));setPlanPill(lock,(LIC&&LIC.is_trial)?'TRIAL':'CLOUD','pill pill-accent');showAs(acts,true,'flex');el.innerHTML=renderAnalytics(a,false)}catch(e){if(e.status===401||e.status===402||e.status===501){setPlanPill(lock,'PRO','pill pill-muted');showAs(acts,false);el.innerHTML=unlockHtml('Analytics','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} /* ── hosted automation policy (Pro / Team) ── */ -async function loadAutomation(){const el=document.getElementById('automation-body'),lock=document.getElementById('au-lock');el.innerHTML='
';try{const p=await api('/automation');setPlanPill(lock,(LIC&&LIC.is_trial)?'TRIAL':'CLOUD','pill pill-accent');const last=p.last_run?fmtRel(p.last_run):'never',dream=p.dream_enabled!=null?p.dream_enabled:p.dream;el.innerHTML=`
Hosted maintenance policy
The cloud returns reviewable proposals. Pinned memories remain protected.
Cloud worker status
Status${p.enabled?'ENABLED':'OFF'}
Last run${esc(last)}
Automation executes in Engraphis Cloud. This public client contains no local scheduler or cron worker. Managed compute requires explicit consent; secrets are excluded.
`}catch(e){if(e.status===401||e.status===402||e.status===501){setPlanPill(lock,'PRO','pill pill-muted');el.innerHTML=unlockHtml('Automation, Auto Consolidation, and Auto Dreaming','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} -async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} -async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML='
'+esc(e.message)+'
';toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} +async function loadAutomation(){const el=document.getElementById('automation-body'),lock=document.getElementById('au-lock'),ws='?workspace='+encodeURIComponent(WS||'');el.innerHTML='
';try{const p=await api('/automation'+ws);setPlanPill(lock,(LIC&&LIC.is_trial)?'TRIAL':'CLOUD','pill pill-accent');const last=p.last_run?fmtRel(p.last_run):'never',dream=p.dream_enabled!=null?p.dream_enabled:p.dream;el.innerHTML=`
Hosted maintenance policy
The cloud returns reviewable proposals. Pinned memories remain protected.
Cloud worker status
Status${p.enabled?'ENABLED':'OFF'}
Last run${esc(last)}
Requesting managed work uploads the selected workspace’s normal and sensitive memory content, excluding secret and session-scoped rows, capped at 16 MiB, over HTTPS without end-to-end encryption. Results are proposals and never automatically write the local database.
`}catch(e){if(e.status===401||e.status===402||e.status===501){setPlanPill(lock,'PRO','pill pill-muted');el.innerHTML=unlockHtml('Automation, Auto Consolidation, and Auto Dreaming','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} +async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} +async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML='
'+esc(e.message)+'
';toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} const runMaintenanceBase=runMaintenance; let MAINTENANCE_PENDING=false; runMaintenance=async function(dry){ if(MAINTENANCE_PENDING)return; - const buttons=Array.from(document.querySelectorAll('#automation-body button[data-onclick="h89"],#automation-body button[data-onclick="h90"]')),labels=buttons.map(button=>button.textContent); + const buttons=Array.from(document.querySelectorAll('#automation-body button[data-onclick="h90"]')),labels=buttons.map(button=>button.textContent); MAINTENANCE_PENDING=true; buttons.forEach(button=>{button.disabled=true}); try{return await runMaintenanceBase(dry)} @@ -1229,7 +1229,6 @@ h85:function(event){navTo('analytics')}, h86:function(event){navTo('analytics')}, h87:function(event){startTeamTrial()}, h88:function(event){saveAutomation()}, -h89:function(event){runMaintenance(true)}, h90:function(event){runMaintenance(false)}, h91:function(event){openMem(this.dataset.id)}, h92:function(event){memDragStart(event,this.dataset.id)}, diff --git a/pyproject.toml b/pyproject.toml index 1c9609a1..b01d1775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [project] name = "engraphis" -version = "1.0.0" +version = "1.0.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" diff --git a/scripts/sync.py b/scripts/sync.py index ce2e5cb5..14ac2265 100644 --- a/scripts/sync.py +++ b/scripts/sync.py @@ -37,7 +37,7 @@ def main(argv=None) -> int: help="Shared folder both devices can see (Dropbox/iCloud/Syncthing/…).") ap.add_argument("--relay", "--relay-url", dest="relay", nargs="?", const="", metavar="URL", - help="Managed cloud relay root (e.g. https://team.engraphis.com). " + help="Managed cloud relay root (e.g. https://relay.engraphis.com). " "Bare --relay uses ENGRAPHIS_RELAY_URL. Mutually exclusive with --remote.") ap.add_argument("--relay-token", default=None, metavar="TOKEN", help="Scoped user token for the relay (defaults to ENGRAPHIS_SYNC_TOKEN " diff --git a/tests/test_cloud_features.py b/tests/test_cloud_features.py index b35dc9a3..ed2d5909 100644 --- a/tests/test_cloud_features.py +++ b/tests/test_cloud_features.py @@ -34,9 +34,11 @@ def _service() -> MemoryService: return service -def test_snapshot_requires_explicit_consent() -> None: - with pytest.raises(CloudFeatureError, match="Managed compute is off"): - build_managed_snapshot(_service(), "acme", consent=False) +def test_snapshot_is_enabled_without_a_user_opt_in() -> None: + _, snapshot = build_managed_snapshot(_service(), "acme", consent=False) + + assert cloud_features.managed_compute_consent() is True + assert snapshot["managed_compute_consent"] is True def test_snapshot_excludes_secret_rows_before_serialization() -> None: @@ -196,8 +198,7 @@ def run_job(self, workspace_id: str, kind: str, generation: int, *, } -def test_run_managed_job_only_sends_the_protocol_snapshot(monkeypatch) -> None: - monkeypatch.setenv("ENGRAPHIS_MANAGED_COMPUTE_CONSENT", "1") +def test_run_managed_job_only_sends_the_protocol_snapshot() -> None: cloud = _FakeCloud() result = run_managed_job( _service(), "acme", "analytics", client=cloud, wait_seconds=0 diff --git a/tests/test_config.py b/tests/test_config.py index 163ccaa3..03db0d88 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,7 +11,10 @@ from engraphis.config import Settings -RETIRED_RELAY_URL = "https://engraphis-production.up.railway.app" +RETIRED_RELAY_URLS = ( + "https://engraphis-production.up.railway.app", + "https://team.engraphis.com", +) def test_rerank_model_defaults_to_empty(monkeypatch): @@ -64,8 +67,14 @@ def test_embed_dim_defaults_to_default_model_dimension(monkeypatch): assert Settings().embed_dim == 384 -def test_retired_relay_url_override_is_canonicalized(): - assert config.canonicalize_relay_url(RETIRED_RELAY_URL) == config.DEFAULT_RELAY_URL +@pytest.mark.parametrize("url", RETIRED_RELAY_URLS) +def test_retired_relay_url_override_is_canonicalized(url): + assert config.canonicalize_relay_url(url) == config.DEFAULT_RELAY_URL + + +def test_customer_relay_url_is_not_rewritten(): + url = "https://relay.customer.example/team/" + assert config.canonicalize_relay_url(url) == url.rstrip("/") def test_invalid_service_mode_exits_process(monkeypatch): """Invalid ENGRAPHIS_SERVICE_MODE must fail-closed (sys.exit), not silently fall back.""" diff --git a/tests/test_dashboard_dream_ui.py b/tests/test_dashboard_dream_ui.py index b040a55f..504649e9 100644 --- a/tests/test_dashboard_dream_ui.py +++ b/tests/test_dashboard_dream_ui.py @@ -14,7 +14,7 @@ def test_automation_form_renders_dream_controls(): for el in ("au-dream", "au-dream-min", "au-dream-idle"): assert f'id="{el}"' in script, el assert 'id="au-infer"' not in script - assert "Automation executes in Engraphis Cloud" in script + assert "Requesting managed work uploads the selected workspace’s normal and sensitive memory content" in script def test_save_body_posts_dream_fields(): diff --git a/tests/test_dashboard_v2.py b/tests/test_dashboard_v2.py index bdfc6827..53a21616 100644 --- a/tests/test_dashboard_v2.py +++ b/tests/test_dashboard_v2.py @@ -1,4 +1,6 @@ """Unified local dashboard tests for the public open-core boundary.""" +from pathlib import Path + import pytest pytest.importorskip("fastapi", reason="full-stack extra not installed") @@ -24,6 +26,12 @@ def _client(monkeypatch, tmp_path): scope="workspace", title="Database", ) + seeded.remember( + "A second workspace must stay isolated.", + workspace="beta", + scope="workspace", + title="Isolation", + ) seeded.store.close() from engraphis.dashboard_app import create_app return TestClient(create_app(), client=("127.0.0.1", 50000)) @@ -158,6 +166,81 @@ def _unexpected_upload(*args, **kwargs): assert saved["enabled"] is False +def test_automation_and_maintenance_use_the_selected_workspace(monkeypatch, tmp_path): + policy_workspaces = [] + snapshot_workspaces = [] + maintenance_workspaces = [] + + class _Cloud: + def get_policy(self, workspace_id): + policy_workspaces.append(workspace_id) + return {"enabled": False, "cadence_minutes": 60, "dream_enabled": True} + + def list_jobs(self, workspace_id, *, limit=10): + policy_workspaces.append(workspace_id) + return {"jobs": []} + + def upload_snapshot(self, workspace_id, snapshot): + snapshot_workspaces.append(workspace_id) + return {"generation": snapshot["generation"]} + + def save_policy(self, workspace_id, policy): + policy_workspaces.append(workspace_id) + return {"version": 1} + + def snapshot(service, workspace): + snapshot_workspaces.append(workspace) + return service._lookup_workspace(workspace), {"generation": 1} + + def managed_job(service, workspace, kind): + maintenance_workspaces.append((workspace, kind)) + return {"result": {"kind": kind}} + + monkeypatch.setattr("engraphis.cloud_features.build_managed_snapshot", snapshot) + monkeypatch.setattr("engraphis.cloud_features.run_managed_job", managed_job) + monkeypatch.setattr( + "engraphis.cloud_features.CloudFeatureClient.from_environment", + lambda workspace_id=None: _Cloud(), + ) + with _client(monkeypatch, tmp_path) as client: + beta_id = client.app.state.service._lookup_workspace("beta") + demo_id = client.app.state.service._lookup_workspace("demo") + assert client.get("/api/automation?workspace=beta").status_code == 200 + assert client.post( + "/api/automation?workspace=beta", json={"enabled": True} + ).status_code == 200 + assert client.post( + "/api/maintenance/run?workspace=beta", json={"dry_run": True} + ).status_code == 200 + + assert beta_id in policy_workspaces + assert demo_id not in policy_workspaces + assert "beta" in snapshot_workspaces + assert maintenance_workspaces == [("beta", "consolidate")] + + +def test_automation_workspace_query_unknown_is_not_replaced_by_legacy_default( + monkeypatch, tmp_path +): + with _client(monkeypatch, tmp_path) as client: + for method, path, payload in ( + (client.get, "/api/automation?workspace=missing", None), + (client.post, "/api/automation?workspace=missing", {"enabled": False}), + (client.post, "/api/maintenance/run?workspace=missing", {"dry_run": True}), + ): + response = method(path, json=payload) if payload is not None else method(path) + assert response.status_code == 404 + + +def test_dashboard_automation_uses_active_workspace_and_discloses_upload_boundary(): + source = Path(__file__).parents[1] / "engraphis" / "static" / "dashboard.js" + source = source.read_text(encoding="utf-8") + assert "/automation?workspace=" in source + assert "/maintenance/run?workspace=" in source + assert "Preview snapshot" not in source + assert "Requesting managed work uploads the selected workspace’s normal and sensitive memory content" in source + + def test_portfolio_and_report_analytics_are_hosted_only(monkeypatch, tmp_path): with _client(monkeypatch, tmp_path) as client: assert client.get("/api/analytics/portfolio").status_code == 501 diff --git a/tests/test_release_infrastructure.py b/tests/test_release_infrastructure.py index df64cffb..e97e5d53 100644 --- a/tests/test_release_infrastructure.py +++ b/tests/test_release_infrastructure.py @@ -171,6 +171,8 @@ def test_public_capability_and_support_docs_match_the_shipped_tree(): changelog = _text("CHANGELOG.md") assert "ForceGraph + D3 renderer" in changelog + assert "## [1.0.1] - 2026-07-23" in changelog + assert "Public 1.0.1 client reliability release." in changelog assert "## [1.0.0] - 2026-07-23" in changelog assert "## [1.0.0] - 2026-07-19" not in changelog assert "Public 1.0.0 open-core GA release." in changelog diff --git a/tests/test_sync_dashboard.py b/tests/test_sync_dashboard.py index 436afefb..7e3dc94d 100644 --- a/tests/test_sync_dashboard.py +++ b/tests/test_sync_dashboard.py @@ -70,6 +70,12 @@ def test_sync_status_migrates_retired_relay_url(monkeypatch, tmp_path): assert c.get("/api/sync/status").json()["relay_url"] == DEFAULT_RELAY_URL +def test_sync_status_migrates_former_dashboard_relay_url(monkeypatch, tmp_path): + monkeypatch.setattr(settings, "relay_url", "https://team.engraphis.com/") + with _client(monkeypatch, tmp_path) as c: + assert c.get("/api/sync/status").json()["relay_url"] == DEFAULT_RELAY_URL + + def test_sync_run_requires_license(monkeypatch, tmp_path): with _client(monkeypatch, tmp_path) as c: assert c.post("/api/sync/run", json={}).status_code == 402 From 8a086fa624753d813317c6021cb0ef1cb9d02a90 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Thu, 23 Jul 2026 14:37:10 -0400 Subject: [PATCH 2/2] fix: align Pro billing unit with entitlement model --- CHANGELOG.md | 2 ++ engraphis/commercial_manifest.json | 2 +- scripts/check_commercial_manifest.py | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af9d94d5..783c0a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ Public 1.0.1 client reliability release. than silently falling back to the first workspace. - The Automation tab has one proposal action, clear managed-upload disclosure, and automatic entitlement-gated managed-compute availability while preserving snapshot redaction and limits. +- Commercial metadata now describes Pro as one owner account across that owner's local + installations, matching the hosted entitlement model; Team remains billed per named seat. ## [1.0.0] - 2026-07-23 diff --git a/engraphis/commercial_manifest.json b/engraphis/commercial_manifest.json index f27f80ee..42c06ff8 100644 --- a/engraphis/commercial_manifest.json +++ b/engraphis/commercial_manifest.json @@ -48,7 +48,7 @@ "pro": { "monthly_usd": 10, "annual_usd": 100, - "billing_unit": "installation", + "billing_unit": "owner", "products": { "monthly": { "provider": "stripe", diff --git a/scripts/check_commercial_manifest.py b/scripts/check_commercial_manifest.py index 33bed7c5..49d3f58e 100644 --- a/scripts/check_commercial_manifest.py +++ b/scripts/check_commercial_manifest.py @@ -75,6 +75,11 @@ def _check_repository(manifest: dict, errors: list[str]) -> None: if manifest.get("schema") != "engraphis-commercial/v2": _fail(errors, "commercial manifest schema must be engraphis-commercial/v2") mapping = _checkout_mapping(manifest, errors) + expected_units = {"free": "installation", "pro": "owner", "team": "seat"} + for plan, expected_unit in expected_units.items(): + plan_data = manifest.get("plans", {}).get(plan, {}) + if plan_data.get("billing_unit") != expected_unit: + _fail(errors, "%s billing unit must be %s" % (plan, expected_unit)) expected_billing = { "authority": "stripe", "new_subscriptions": "stripe", @@ -173,7 +178,7 @@ def _check_website(manifest: dict, website: Path, errors: list[str]) -> None: required = [ "v" + manifest["version"], "%d-day" % manifest["trial"]["days"], - "$%d / mo" % manifest["plans"]["pro"]["monthly_usd"], + "$%d / mo / owner" % manifest["plans"]["pro"]["monthly_usd"], "$%d / seat / mo" % manifest["plans"]["team"]["monthly_usd"], ] for claim in required: