Fix licensing gating, cloud failure modes and update hangs for launch - #73
Conversation
Closes defects that denied paying customers what they bought, hung or crashed the client when the cloud was unreachable, and leaked a credential into logs. Each fix carries a regression test. Paying customers were denied what they bought - /api/license always returned an empty feature list, so Analytics, Automation and Team rendered with PRO/TEAM lock badges for customers who had paid for them. It now reports the entitled feature set. - The plan is read from the registration and refresh handshake, which now discloses it, with an explicit precedence: ENGRAPHIS_CLOUD_PLAN override, then the persisted session plan, then the cached entitlement, then inference. A Team customer previously had to set an undocumented environment variable or be shown PRO. - Team upgrade links pointed at the Pro page. - A 409 consent_required is no longer treated as a billing answer. It was folded into the entitlement check, so a paying customer who had not set ENGRAPHIS_MANAGED_COMPUTE_CONSENT=1 was shown a panel telling them to buy the plan they already owned. Consent is now routed ahead of entitlement at every hosted call site. - loadSyncStatus rendered the purchase panel for every failure, including a transient network error or a 5xx, so a dropped connection told a paying customer to buy Pro. - /memory/license reported plan "local" for a connected paying customer, so the two license surfaces contradicted each other. Cloud failure modes - Draining an HTTP error body inside the except block let a timeout escape as an unhandled TimeoutError, surfacing as an opaque 500 with a traceback exactly when the cloud was flaky. - A DNS or resolution failure raised the same error as a genuinely malformed URL and was reported to the customer as a non-transient configuration problem with no retry. - Lapsed billing was indistinguishable from an outage: 402, 429 and 404 all collapsed into one 503, so a past_due customer retried forever instead of being sent to billing. - Connection attempts shared one deadline instead of giving each vetted address the full timeout, which turned a 10s refresh budget into N x 10s while holding the exclusive cross-process refresh lock. - PinnedHTTPSConnection pins a default connect timeout rather than inheriting urllib's block-forever default. Credential handling - The access token is excluded from the client's repr. The dataclass default printed a live bearer token into any traceback frame or debug log. - update_check.py uses the pinned HTTPS opener, so the one credential-adjacent request with a user-controllable endpoint is no longer exempt from the repo's SSRF and DNS-rebinding vetting. Updater - scripts/update.py had nineteen untimed network subprocess calls, all with captured output, so engraphis-update hung silently and indefinitely against a stalled index or unreachable remote. A stall mid-upgrade left the tree checked out to the new tag with the rollback unreached. Every call is now bounded and the rollback runs on timeout. Test suite: ~1209 -> 1363 passing, plus 11 end-to-end tests. The end-to-end suite previously only ever exercised a free customer, which is why a paying user seeing a lock badge survived this long; it now covers Pro, Team and lapsed Team.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3cf76bee76
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…e floor
tests/test_client_launch.py and tests/test_hosted_plan_resolution.py import
engraphis.routes.v2_api, which imports FastAPI. The core-py39 CI job installs
numpy and pytest only, so both modules raised ModuleNotFoundError at collection
and interrupted the whole run rather than skipping.
Guard both with pytest.importorskip("fastapi"), matching the convention already
used by tests/test_agent_connect.py and tests/test_app_auth.py.
Verified against a numpy+pytest-only virtualenv: the suite collects and passes
with these two modules skipped, and the full extras install is unchanged at
1363 passed, 7 skipped.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b28417327e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Customers were told to hand-edit ENGRAPHIS_MANAGED_COMPUTE_CONSENT=1 in a .env file before Analytics or Automation would work. Consent now travels with the cloud account: connecting an installation accepts the terms that cover managed compute, so a connected installation is allowed by default and the variable is never shown to a user. An installation with no cloud session is still never allowed -- there is no account, so there is no agreement to rely on. The variable survives as an operator override only; =0 opts a connected installation back out. Also fixes two predicates that were inverted against their own comment. managedConsentRequired() was guarded on CURRENT_VIEW!=='analytics' && CURRENT_VIEW!=='automation', but its only four call sites run in exactly those two views, so it could never return true. hostedFeatureUnavailable() returned true for any error in those views regardless of status. Together a paying Pro customer hitting a 409, a 500, or a dropped connection was shown a panel selling Pro -- the exact regression the comment above them describes. Both are now status-only. The upload disclosure is deliberately kept: what is uploaded, the 16 MiB cap, and that it travels over HTTPS without end-to-end encryption. Two tests asserted the defect as intended behaviour and are replaced; the weakened predicate assertion is restored and mutation-tested. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
access_for_workspace merged the refresh answer onto the saved record with dict.update. _declared_entitlement deliberately omits cloud_features when the body carries no feature list, so a downgrade replaced plan while the previous plan's grants survived. A Team -> Pro downgrade therefore reported plan "pro" with "team" still in features, leaving the Team tab unlocked indefinitely. Fixed at the merge site so _declared_entitlement keeps its tested contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
subprocess.run(capture_output=True, timeout=N) does not honour N on Windows: after TimeoutExpired, CPython calls communicate() with no timeout, and the reader threads block until every inherited pipe write handle closes -- including grandchildren. git fetch and git ls-remote always spawn git-remote-https, so the three network calls stayed unbounded. Measured with a grandchild holding the pipe: a 3s budget returned after 30.2s before, and raises at 3.1s now. Network calls that need stdout go through a bounded Popen reader that kills the process tree and re-drains with a cap; the rest stop capturing. Every git call now gets stdin=DEVNULL and GIT_TERMINAL_PROMPT=0 so an expired token cannot park the updater on a credential prompt. Also moves _detect_install() inside main()'s try, so a stalled pip show prints its crafted message instead of a traceback. Three existing tests encoded the belief that timeout= alone was sufficient and are rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review + three fixes pushed (
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e3a23f2548
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Greptile SummaryThis is a launch-hardening pass fixing three categories of bugs: paying customers being denied features they bought (empty feature list, wrong plan inference, every failure showing the upgrade panel), cloud failure modes (unhandled
Confidence Score: 4/5Safe to merge; one edge-case in the entitlement denial path only affects older control planes with no prior entitlement cache, and even then the functional guards (402 on every cloud call) remain in place. The core licensing fixes are well-structured: the session → entitlement-cache → inference precedence chain is correct, and plan-change stale-feature-list eviction is handled. The one gap is in _deny_entitlement_cache: when there is no pre-existing cache entry for the current organisation (possible on an older control plane that never returns plan in DeviceRegistrationResponse), a 402 billing denial is silently discarded. _plan_entitlement then falls to the connected inference branch and returns pro with cloud_access_active=True, so the dashboard badge shows paid features as unlocked even after billing lapses. All actual cloud operations still fail with 402, so this is a presentation disagreement rather than a capability bypass — but it is a present defect on the changed path. Files Needing Attention: engraphis/routes/v2_api.py — _deny_entitlement_cache (lines 392–424): the
|
| Filename | Overview |
|---|---|
| engraphis/routes/v2_api.py | Large new entitlement resolution stack (session → entitlement cache → inference) with background refresh thread; one edge-case in _deny_entitlement_cache leaves the pro badge active for lapsed older-CP deployments with no prior cache entry |
| engraphis/cloud_session.py | Adds _declared_entitlement, saved_entitlement, record_billing_denial; fixes HTTP-error-body drain TimeoutError, workspace_id=null payload, and credential repr leak; plan-change stale-feature-list guard is correct |
| scripts/update.py | All 19 subprocess calls now bounded via _bounded_call with process-tree teardown; rollback catches UpdateTimeout so a stalled checkout no longer strands the tree at the new tag |
| engraphis/hosted_client.py | PinnedHTTPSConnection now pins a bounded default connect timeout and splits the caller's budget across all vetted addresses instead of giving each one the full allowance |
| engraphis/cloud_features.py | managed_compute_consent now grants cloud-connected installs by default; _public_session_error separates billing denial from outage; access_token repr=False plugs token leak in tracebacks |
| engraphis/static/dashboard.js | loadSyncStatus now routes by error cause instead of showing the upgrade panel for every failure; loadAnalytics/loadAutomation distinguish consent-required from billing-unavailable |
| engraphis/routes/memory.py | GET /memory/license now delegates to hosted_plan_summary() so both license surfaces report the same plan instead of always returning "local" |
| engraphis/update_check.py | Uses build_pinned_https_opener instead of plain build_opener so the user-controllable ENGRAPHIS_UPDATE_URL gets the same SSRF/DNS-rebinding vetting as other outbound clients |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["/api/license or /api/bootstrap"] --> B["hosted_plan_summary()"]
B --> C["_plan_entitlement()"]
C --> D{"ENGRAPHIS_CLOUD_PLAN set?"}
D -- Yes --> E["Return override plan"]
D -- No --> F{"cloud_session.configured?"}
F -- No --> G["Return local / no features"]
F -- Yes --> H["_session_entitlement()\n(session file, no network)"]
H -- non-empty --> I["Schedule background refresh if stale\nReturn session plan + features"]
H -- empty --> J["_read_entitlement_cache()\n(entitlement cache file)"]
J -- non-empty --> K["Schedule background refresh if stale\nReturn cached plan + features"]
J -- empty --> L["Inference: return 'pro'\n(schedule refresh)"]
I --> M["/api/license response"]
K --> M
E --> M
G --> M
L --> M
subgraph Background refresh thread
N["_fetch_authoritative_entitlement()"]
N --> O["access_for_workspace(None)\ntoken refresh"]
O -- plan in response --> P["_session_entitlement non-empty → done"]
O -- no plan older CP --> Q["GET /v1/entitlements/{org}"]
Q --> R["_write_entitlement_cache()"]
O -- 402 billing lapsed --> S["record_billing_denial()\n_deny_entitlement_cache()"]
end
Reviews (6): Last reviewed commit: "fix(cloud): serialize the denial write a..." | Re-trigger Greptile
Senior Dev Review ✅This is an exceptionally well-engineered launch-hardening PR. The scope is large (~5000 lines) but every change traces back to a concrete customer-impacting defect, and the test coverage is thorough. Strengths1. Subprocess timeout enforcement is correct 2. Entitlement resolution architecture is sound 3. Dashboard error routing is now correct 4. Connection timeout budget sharing Minor Observations (non-blocking)
Test CoverageThe test suite is comprehensive: launch guards, offline behavior, malformed responses, process tree killing, timeout enforcement, plan resolution, cache corruption, and E2E dashboard rendering. The Ready to merge. 🚀 |
Coding-Dev-Tools
left a comment
There was a problem hiding this comment.
🔍 Pre-PR Code Analyzer Review
Verdict: REQUEST_CHANGES (coordination gate)
Analysis Summary
This is a well-structured, ~5000-line hardening PR covering licensing gating, cloud failure modes, credential handling, and updater reliability. The engineering quality is high.
✅ Strengths
- Security fixes are sound:
access_token = field(repr=False)correctly prevents bearer token leakage in tracebacks/logs. The_NoRedirecthandler andvalidate_cloud_base_url()on every outbound path close SSRF/DNS-rebinding vectors. - Subprocess budget enforcement is correct: The
_kill_process_tree()+ bounded drain pattern properly handles the Windowscommunicate()deadlock where grandchildren (git-remote-https) hold pipe handles pastTimeoutExpired. The_OWN_PROCESS_GROUP/taskkill /Tsplit is the right POSIX/NT divergence. - Error mapping is actionable:
_refresh_http_error()distinguishes billing (402/429) from outage (5xx) from auth (401/403), preventing the "paying customer told to buy Pro" regression. - Test coverage: 1209→1363 passing + 11 e2e tests covering Pro/Team/lapsed scenarios that previously only exercised free tier.
- CI: All green (CodeQL, py3.10/3.11/3.12, browser smoke, docker, wheel build).
⚠️ Coordination Gate (not a code defect)
This PR has 2 distinct contributors (Coding-Dev-Tools/Jaixii + Claude Opus 5 co-authorship). Per swarm policy, approval requires ≥3 distinct agent contributors or Sentinel coordination. This is a procedural hold, not a quality objection.
Minor Observations (non-blocking)
_ENTITLEMENT_REFRESH_LOCKglobal lock — contention is unlikely but a future multi-workspace daemon could serialize refreshes unnecessarily. Document the assumption or consider per-workspace locks if the architecture evolves._run_capturedstderr-on-terminal design — good choice for surfacing git errors, but note that in non-interactive cron contexts stderr may be discarded. Acceptable trade-off.- Consent semantics change — moving from explicit opt-in (
ENGRAPHIS_MANAGED_COMPUTE_CONSENT=1) to opt-out (=0) is a UX/policy decision that should be called out in release notes for existing deployments.
Conclusion
Code is merge-ready from a quality and security standpoint. The REQUEST_CHANGES is purely the multi-contributor coordination gate — once Sentinel signs off or a third contributor reviews, this should proceed.
Automated review by Pre-PR Code Analyzer (Hermes cron)
Addresses three review findings on PR #73. The cached entitlement was served without comparing its stored organization_id against the organization the installation is bound to now. Reconnecting or re-pinning to a different organization while reusing the same state directory therefore displayed the PREVIOUS organization's plan and features -- for up to the refresh interval, or indefinitely when refresh is disabled. The session path already rejected this mismatch; the cache now does too, and fails closed so a rejected cache immediately schedules the correcting fetch. hosted_plan_summary() resolved _plan_entitlement() twice, once directly and once via _hosted_plan(), doubling the cloud-session and cache reads -- and the chance of scheduling a background refresh -- on every /api/license and /api/bootstrap. The _hosted_plan override seam that tests rely on is preserved: when a caller rebinds it, its answer still wins. access_for_workspace() was annotated workspace_id: str while the org-scoped entitlement read passes None, so the refresh body carried "workspace_id": null. A control plane that requires a string would 4xx, and the whole compatibility fallback silently returns None, meaning the cache from GET /v1/entitlements/{org} would never be written for older control planes. The key is now omitted rather than sent as null. Also passes a _NoRedirect instance rather than the class in update_check, matching every other caller. Verified behaviour was already identical -- build_opener instantiates a passed class -- so this is consistency only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses two more review findings on PR #73. _run() still reached subprocess.run(timeout=...) for non-captured steps, which kills only the immediate child. pip's resolver or git-remote-https could outlive the budget and keep touching the repository while the caller had already started rollback. Every step now goes through one spawn path with process-tree termination and a bounded post-kill drain, so capture selects whether stdout is piped rather than whether the budget is enforceable at all. git checkout tags/<tag> -- the destructive step -- ran above the rollback try, so a timed-out or non-zero checkout raised straight past it and left an editable install partially switched while reporting only a timeout. It now sits inside the same boundary. A killed checkout can also strand .git/index.lock, which blocks every later git command: the lock is removed only when it was absent before our checkout started and the checkout we killed is the plausible author, and otherwise it is named for the user rather than deleted, so a lock held by another live git process is never touched. The rollback checkout now runs with check=True. A failed restore used to be silent while main() still reported the previous installation restored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review threads resolved — what fixed each
Two of these (the P1 predicate inversion and the VerificationOffline gate per Each fix ships with a regression test that was verified to fail before and pass after — including the previously- Still open, deliberately not fixed here
🤖 Generated with Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1f9670753
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Addresses a review finding on PR #73. A lapsed subscription answers 402 on token refresh. That is the control plane's authoritative billing answer, but it was caught by the same broad handler as offline/revoked/invalid and discarded. Because the saved session outranks the entitlement cache, cloud_access_active stayed true and the dashboard kept advertising paid features indefinitely while every hosted operation was denied -- the same two-surfaces-disagree failure this branch set out to fix. record_billing_denial() persists the denial: the access flag and the grants are cleared, the plan name is kept so the UI can still say which plan lapsed. Only 402 does this; an outage must never look like a cancellation. Regression tests both ways -- verified that the denial test fails without the fix (cloud_access_active stayed true with the full feature list) and that a 503 leaves a paying customer's access intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
P2 — invalidate cached active state after a billing denial: fixed in A 402 was caught by the same broad handler as offline/revoked/invalid and discarded. Since the saved session outranks the entitlement cache,
Verified both directions: the denial test fails without the fix ( 🤖 Generated with Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eab8a4c5c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Follow-up to eab8a4c, which settled only the session record. Against an older control plane the refresh carries no entitlement fields, so that session stays planless: saved_entitlement() keeps answering {} and _plan_entitlement falls through to the compatibility cache, which went on advertising the paid plan while every cloud operation was denied. The denial has to settle both layers or it settles neither. _deny_entitlement_cache() clears the cached grants and access flag on 402 and keeps the plan name so the UI can still say which plan lapsed. Verified the new test fails without it -- the cache still reported cloud_access_active true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
P2 — invalidate the compatibility cache on billing denial: fixed in Against an older control plane the refresh carries no entitlement fields, so that session stays planless:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0cfc1e52e5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Addresses four more review findings on PR #73. 401 was drawing the purchase panel. The cloud maps it to "the cloud session expired or was revoked; connect again" -- a credential problem, not a billing one -- so an already-paying customer whose refresh credential lapsed was sold the plan they already own. Only 402 and 501 are billing answers now; 401 falls through to the error branch where the reconnect instruction is what the customer reads. _managed_call flattened every CloudFeatureError into "managed cloud operation failed", so 429, 5xx and a non-consent 409 were indistinguishable and the actionable copy from _public_http_error / _public_session_error never reached anyone. Those messages are fixed local literals -- audited all 19 construction sites in cloud_features, the only interpolation being upgrade_url() and one template whose substituted value is one of two fixed strings -- so they pass through, bounded and control-character-checked, with the old placeholder kept as the fallback. An AST test now pins that invariant so a future f-string or reflected response field fails. Billing denials went unstamped: entitlement_checked_at never advanced and a repeat denial wrote nothing, so the 15-minute interval never suppressed the next attempt and every request rotated the credential again against a control plane that had already answered 402. Every denial is now stamped, including the repeat, in both the session record and the compatibility cache. Also replaces three PEP 604 dict | None annotations in scripts/update.py with Optional[dict]. Contrary to the finding these did not break import -- from __future__ import annotations keeps them as strings, verified on real 3.9.25 -- but typing.get_type_hints() did raise there, and the house style is 3.9-compatible syntax. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 5 — all four findings fixed in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8da56a277d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two review findings, both on code added earlier in this branch. The organization guard rejected only a mismatch. A fresh bootstrap carries just a refresh credential and a control URL, so the current organization is unknown until the first refresh names it -- and in that window a reused state directory served whatever it still held, i.e. the previous organization's paid plan, indefinitely with the refresh disabled. A cache scoped to an organization now requires a positive match, not merely the absence of a contradiction. record_billing_denial did an unguarded load-modify-save on the shared session file. Another worker rotating the credential concurrently could have its new value overwritten with the stale one; the control plane treats a spent credential as replay and revokes the whole family, so a lapsed subscription would have become a forced reconnect. The read, modify and save now happen inside the same _refresh_lock() that access_for_workspace rotates under. Verified the 402 reaches the handler with its status intact (a real 402 propagates out of access_for_workspace unrewritten) and that the lock is not re-entered on that path -- it has already unwound when the handler runs, which matters because _refresh_lock is not re-entrant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 6 — both fixed in
|
There was a problem hiding this comment.
💡 Codex Review
engraphis/engraphis/hosted_client.py
Line 180 in e3f9f78
When a multi-homed endpoint's first address blackholes instead of failing immediately, this gives that address the entire remaining timeout; after it expires, both connection loops break without trying a later reachable address. A common broken-IPv6/working-IPv4 setup therefore loses the existing fallback and makes hosted operations fail consistently. Divide the remaining budget among the remaining candidates, or otherwise cap each attempt while preserving the overall deadline.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
Launch-hardening pass over the client's licensing surface, its behaviour when the cloud is slow or down, and the updater.
Test suite: ~1209 → 1363 passing, 7 skipped, plus 11 end-to-end tests (was 7).
ruffclean,node --checkclean, every touched module parses underast.parse(feature_version=(3,9)). No new dependencies.No DRM or bypass hardening was attempted — this is a public repo, local gates are always patchable, and it is not where the risk is. The risk is paying customers being denied, the client hanging, and credentials leaking.
Paying customers were denied what they bought
/api/licensealways returned an empty feature list, so Analytics, Automation and Team rendered withPRO/TEAMlock badges for customers who had paid for them.409 consent_requiredwas treated as a billing answer. It was folded into the entitlement check, so a paying customer who had not yet setENGRAPHIS_MANAGED_COMPUTE_CONSENT=1was shown a panel telling them to buy the plan they already owned. Consent is now routed ahead of entitlement at every hosted call site.loadSyncStatusrendered the purchase panel for every failure — a dropped connection or a 5xx told a paying customer to buy Pro.PROunless they set an undocumented environment variable. The plan now comes from the registration/refresh handshake, with documented precedence:ENGRAPHIS_CLOUD_PLANoverride → persisted session plan → cached entitlement → inference. Falls back cleanly against a control plane that predates the new fields./memory/licensereportedplan: "local"for a connected paying customer, contradicting/api/license.Cloud failure modes
TimeoutError→ opaque 500 with a traceback, from draining an HTTP error body inside theexceptblock — exactly when the cloud was flaky.past_duecustomer retried forever instead of being sent to billing.PinnedHTTPSConnectionpins a default connect timeout instead of inheriting urllib's block-forever default.Credentials
__repr__— any traceback frame or%rdebug log leaked a live bearer token. Nowrepr=False.update_check.pyuses the pinned HTTPS opener, so the one credential-adjacent request with a user-controllable endpoint (ENGRAPHIS_UPDATE_URL) is no longer exempt from the repo's SSRF and DNS-rebinding vetting.Updater
scripts/update.pyhad nineteen untimed network subprocess calls, all with captured output, soengraphis-updatehung silently and indefinitely against a stalled index or unreachable remote. A stall mid-upgrade left the tree checked out to the new tag with the rollback unreached — a wedged half-upgrade on a customer's machine. Every call is now bounded, and the rollback runs on timeout.Test coverage note
The end-to-end suite previously only ever exercised a free customer (
plan: 'local', features: []), which is precisely why a paying user seeing a lock badge survived this long. It now covers Pro, Team, lapsed Team and the consent path.Deferred
engraphis/routes/memory.pyv1 surface is corrected rather than frozen;tests/test_v1_licensing.pypins the unconnected case, which is still true and passes unchanged.os.fchmodis absent); POSIX is correct. Fixing it needspywin32, which the dependency floor forbids.getaddrinfocannot be given a timeout and runs outside the connect budget; a wedged resolver still blocks for the OS limit.Verifying