Skip to content

Fix licensing gating, cloud failure modes and update hangs for launch - #73

Merged
Coding-Dev-Tools merged 12 commits into
mainfrom
launch/payments-licensing-hardening
Jul 26, 2026
Merged

Fix licensing gating, cloud failure modes and update hangs for launch#73
Coding-Dev-Tools merged 12 commits into
mainfrom
launch/payments-licensing-hardening

Conversation

@Coding-Dev-Tools

Copy link
Copy Markdown
Owner

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). ruff clean, node --check clean, every touched module parses under ast.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/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.
  • A 409 consent_required was treated as a billing answer. It was folded into the entitlement check, so a paying customer who had not yet 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 — a dropped connection or a 5xx told a paying customer to buy Pro.
  • Team customers saw PRO unless they set an undocumented environment variable. The plan now comes from the registration/refresh handshake, with documented precedence: ENGRAPHIS_CLOUD_PLAN override → persisted session plan → cached entitlement → inference. Falls back cleanly against a control plane that predates the new fields.
  • Every Team upgrade click landed on the Pro page.
  • /memory/license reported plan: "local" for a connected paying customer, contradicting /api/license.

Cloud failure modes

  • Unhandled TimeoutError → opaque 500 with a traceback, from draining an HTTP error body inside the except block — exactly when the cloud was flaky.
  • A DNS failure was reported as "your configuration is invalid", non-transient, with no retry offered.
  • Lapsed billing was indistinguishable from an outage — 402/429/404 all collapsed to one 503, so a past_due customer retried forever instead of being sent to billing.
  • Connection attempts now share one deadline instead of each vetted address getting the full timeout, which turned a 10 s refresh budget into N × 10 s while holding the exclusive cross-process refresh lock.
  • PinnedHTTPSConnection pins a default connect timeout instead of inheriting urllib's block-forever default.

Credentials

  • The access token was printed by the dataclass __repr__ — any traceback frame or %r debug log leaked a live bearer token. Now repr=False.
  • update_check.py uses 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.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 — 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.py v1 surface is corrected rather than frozen; tests/test_v1_licensing.py pins the unconnected case, which is still true and passes unchanged.
  • Windows cannot honour the 0600 intent on the credential file (os.fchmod is absent); POSIX is correct. Fixing it needs pywin32, which the dependency floor forbids.
  • getaddrinfo cannot be given a timeout and runs outside the connect budget; a wedged resolver still blocks for the OS limit.

Verifying

python3 -m pytest          # note: pyproject sets addopts="-q"; do not add another -q
python3 -m ruff check .
npx playwright test        # 11 passing

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread engraphis/static/dashboard.js Outdated
Comment thread engraphis/routes/v2_api.py
Comment thread scripts/update.py
…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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/update.py Outdated
Coding-Dev-Tools and others added 3 commits July 26, 2026 04:13
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>
@Coding-Dev-Tools

Copy link
Copy Markdown
Owner Author

Review + three fixes pushed (b284173..e3a23f2)

Why CI was red

The two new e2e specs were correct and caught a real regression this PR introduced.

managedConsentRequired() was guarded on CURRENT_VIEW!=='analytics' && CURRENT_VIEW!=='automation', but its only four call sites — loadAnalytics, loadAutomation, saveAutomation, runMaintenance — all run in exactly those two views, so it could never return true. Meanwhile hostedFeatureUnavailable() returned true for any error in those views regardless of status.

Net effect: a paying Pro customer hitting a 409, a 500, or a dropped connection was shown a panel selling Pro — verbatim the regression the comment at dashboard.js:188-196 describes and the same bug this PR fixes in loadSyncStatus.

The PR also contained two tests asserting opposite behaviour for the same input: test_a_consent_required_conflict_is_answered_with_a_purchase_panel demanded the upgrade panel, commercial.spec.js:433 demanded the consent copy. And the assertion that would have caught it was weakened from a verbatim predicate check to a substring-anywhere check that passes on a comment.

What changed

1. Consent now travels with the cloud account (product decision). Customers were told to hand-edit ENGRAPHIS_MANAGED_COMPUTE_CONSENT=1 in a .env file. Connecting an installation to Engraphis Cloud now accepts the terms covering managed compute, so a connected install is allowed by default and the variable is never shown to a user. An installation with no cloud session is still never allowed. The variable remains an operator override; =0 opts back out.

The upload disclosure is deliberately retained — what is uploaded, the 16 MiB cap, and that it travels over HTTPS without end-to-end encryption.

Also fixed: deploy/railway-template.json hardcoded "value": "0", which under the new default would have silently opted every Railway deployment out.

2. Entitlement leak on downgrade. access_for_workspace merged with dict.update, and _declared_entitlement omits cloud_features when the body carries no list — so a Team→Pro downgrade replaced plan while the old grants survived. /api/license 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.

3. The Windows update-hang fix did not hold. 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 handle closes, including grandchildren. git fetch/git ls-remote always spawn git-remote-https. Measured with a grandchild holding the pipe: a 3s budget returned after 30.2s before, and raises at 3.1s now. Every git call also gets stdin=DEVNULL and GIT_TERMINAL_PROMPT=0. _detect_install() moved inside main()'s try so a stalled pip show prints its message instead of a traceback.

Verification

Offline gate green on the committed tree in an isolated worktree: 1,376 passed / 0 failed / 0 errors, ruff clean, both eval harnesses at recall@k 1.000, ablation unchanged, asset-drift and commercial-manifest gates pass.

Not included

An unrelated graph-engine web UI (engraphis-graph.js, dashboard.css, index.html) is in progress in the same working tree. It is deliberately excludeddashboard.js was staged with only the consent hunks so that work stays with its author.

Still open (not fixed here)

  • v2_api.py:2188 — the "connected ⇒ pro" fallback is the default against cloud main, which does not yet return plan/cloud_features. Every connected free install currently renders a PRO badge.
  • No negative caching on the background entitlement refresh: a failing control plane re-arms a refresh on every /api/license, rotating the single-use credential each time.
  • unlockHtml silently dropped the "Local-only write grace is separate, capped at 24 hours" disclosure along with its two e2e assertions, unmentioned in the commit message.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/update.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

This 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 TimeoutError from error-body drains, DNS failures misreported as permanent misconfiguration, lapsed billing indistinguishable from outage), and the updater hanging silently on any stalled network call.

  • Licensing: /api/license now returns the entitlement the control plane put on the session (session record → entitlement cache → inference), fixing both the hardcoded empty feature list and the hardcoded "local" plan on /memory/license; the dashboard distinguishes consent-required (409) from billing-unavailable (402/501) from generic errors.
  • Cloud failure modes: _post_refresh now captures the HTTP status before draining the error body (preventing a TimeoutError from escaping the handler), DNS resolution failures are reclassified as retryable outages instead of permanent misconfigurations, and PinnedHTTPSConnection shares one deadline across all vetted addresses.
  • Updater: all 19 subprocess calls are now bounded via _bounded_call, which tears down the whole process tree on timeout; the git checkout + pip install rollback now catches UpdateTimeout so a stalled checkout no longer strands the tree at the new tag.

Confidence Score: 4/5

Safe 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 if not cached: return early exit leaves no denial record for the entitlement cache when starting from a blank-slate older-CP deployment.

Important Files Changed

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
Loading

Reviews (6): Last reviewed commit: "fix(cloud): serialize the denial write a..." | Re-trigger Greptile

Comment thread engraphis/routes/v2_api.py Outdated
Comment thread engraphis/routes/v2_api.py
Comment thread engraphis/update_check.py Outdated
@Coding-Dev-Tools

Copy link
Copy Markdown
Owner Author

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.

Strengths

1. Subprocess timeout enforcement is correct
The key insight that subprocess.run(capture_output=True, timeout=N) does NOT actually enforce N (because communicate() drains pipes unbounded after killing the child) is exactly right. The _run_captured helper that kills the whole process tree before draining, with a bounded drain, is the correct fix. The AST-based test that verifies no unbounded subprocess calls exist is a great regression guard.

2. Entitlement resolution architecture is sound
The layered fallback chain (session record → cached entitlement file → background refresh → connected fallback → local) is well-designed. The background refresh on a daemon thread keeps boot non-blocking. The cache invalidation on plan downgrade (dropping stale cloud_features when plan changes) closes a real security hole where Team features survived a downgrade to Pro.

3. Dashboard error routing is now correct
The separation of managedConsentRequired (409 + consent_required) from hostedFeatureUnavailable (401/402/501) fixes the bug where paying customers were shown purchase panels. The Node.js-based routing tests that execute the actual shipped JS are a strong verification approach.

4. Connection timeout budget sharing
The PinnedHTTPSConnection fix that shares one deadline across all resolved addresses (instead of giving each address the full timeout) prevents N×timeout hangs on multi-homed endpoints. The CloudUrlUnresolved exception type correctly distinguishes reachability failures from configuration errors.

Minor Observations (non-blocking)

  • The _ENTITLEMENT_REFRESH_SECONDS = 15 * 60 default seems reasonable but could benefit from a comment about why 15 minutes specifically.
  • The _NoRedirect handler returning None from redirect_request is correct for blocking redirects, but worth noting this will raise HTTPError rather than silently following.
  • The dashboard JS routing tests depend on node being available - the skipif guard handles this correctly.

Test Coverage

The test suite is comprehensive: launch guards, offline behavior, malformed responses, process tree killing, timeout enforcement, plan resolution, cache corruption, and E2E dashboard rendering. The test_hosted_plan_resolution.py file alone has ~1000 lines of targeted tests.

Ready to merge. 🚀

@Coding-Dev-Tools Coding-Dev-Tools left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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 _NoRedirect handler and validate_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 Windows communicate() deadlock where grandchildren (git-remote-https) hold pipe handles past TimeoutExpired. The _OWN_PROCESS_GROUP / taskkill /T split 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)

  1. _ENTITLEMENT_REFRESH_LOCK global 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.
  2. _run_captured stderr-on-terminal design — good choice for surfacing git errors, but note that in non-interactive cron contexts stderr may be discarded. Acceptable trade-off.
  3. 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)

Coding-Dev-Tools and others added 2 commits July 26, 2026 04:49
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>
@Coding-Dev-Tools

Copy link
Copy Markdown
Owner Author

Review threads resolved — what fixed each

Finding Fixed in
P1 dashboard.js — consent/transient errors misclassified in hosted views 09d4482CURRENT_VIEW guards removed from both predicates; consent classified by status/code regardless of view, hostedFeatureUnavailable limited to 401/402/501
P2 v2_api.py — entitlement cache served across organizations 8abc7eb — cached organization_id compared against pinned/saved org, fails closed; 2 regression tests incl. the refresh-disabled "indefinitely" case
P2 update.py — detection timeout outside the error boundary e3a23f2_detect_install() moved inside main()'s try (line 493)
P2 update.py — timed checkout outside the rollback boundary d1f9670 — checkout moved inside; stranded .git/index.lock removed only when ours, otherwise named not deleted
P1 update.py — descendants survive _run timeout d1f9670 — single spawn path with process-tree termination + bounded post-kill drain; regression test uses real processes and a grandchild sentinel
P2 v2_api.pyhosted_plan_summary() resolves entitlement twice 8abc7eb — resolved once; _hosted_plan consulted only when rebound, so the test seam still wins
P2 v2_api.pyaccess_for_workspace sent "workspace_id": null 8abc7eb — now Optional[str]; _post_refresh omits the key rather than sending null
P2 update_check.py — handler class vs instance 8abc7eb — now passes _NoRedirect(). Correction to the finding: verified empirically that build_opener instantiates a passed class, so the no-redirect guard was already active. Consistency only — not a fixed vulnerability.

Two of these (the P1 predicate inversion and the _detect_install boundary) were found independently by both the bot review and a manual pass, which is decent corroboration they were real rather than stylistic.

Verification

Offline gate per CLAUDE.md: 1,388 passed / 0 failed / 0 errors (1,402 collected), ruff check . clean, both eval harnesses at recall@k 1.000, ablation unchanged (PPR 1.0 on the multi-hop set). All required CI contexts green on d1f9670.

Each fix ships with a regression test that was verified to fail before and pass after — including the previously-xfailed test_the_entitlement_refresh_never_sends_a_null_workspace_id, whose marker is now removed since the cloud_session.py half landed.

Still open, deliberately not fixed here

  • v2_api.py:2188 — the "connected ⇒ pro" fallback is the default against cloud main, which does not yet return plan/cloud_features, so every connected free install currently renders a PRO badge. Needs a product call, not a patch.
  • No negative caching on the background entitlement refresh: a failing control plane re-arms on every /api/license, rotating the single-use credential each time.
  • unlockHtml silently dropped the "Local-only write grace is separate, capped at 24 hours" disclosure with its two e2e assertions, unmentioned in the original commit message.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread engraphis/routes/v2_api.py Outdated
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>
@Coding-Dev-Tools

Copy link
Copy Markdown
Owner Author

P2 — invalidate cached active state after a billing denial: fixed in eab8a4c.

A 402 was caught by the same broad handler as offline/revoked/invalid and discarded. Since the saved session outranks the entitlement cache, cloud_access_active stayed true and the dashboard kept advertising paid features indefinitely while every hosted call was denied.

record_billing_denial() now persists the denial — access flag and grants cleared, plan name kept so the UI can still say which plan lapsed. Only 402 does this; an outage must never look like a cancellation.

Verified both directions: the denial test fails without the fix (cloud_access_active stayed True with the full Team feature list), and a 503 leaves a paying customer's access intact.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread engraphis/routes/v2_api.py
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>
@Coding-Dev-Tools

Copy link
Copy Markdown
Owner Author

P2 — invalidate the compatibility cache on billing denial: fixed in 0cfc1e5. Good catch — eab8a4c 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, preserving the plan name. Verified the new test fails without it — the cache still reported cloud_access_active: true.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread engraphis/cloud_session.py
Comment thread engraphis/static/dashboard.js Outdated
Comment thread engraphis/static/dashboard.js
Comment thread scripts/update.py Outdated
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>
@Coding-Dev-Tools

Copy link
Copy Markdown
Owner Author

Round 5 — all four findings fixed in 8da56a2

P2 — route cloud-session 401s to reconnection. Correct, and the same defect class as the original P1. The cloud maps 401 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 being sold the plan they own. hostedFeatureUnavailable is now 402 || 501; 401 falls through to the error branch. Added a py test (401 joins 400/500/503) and an e2e test asserting no upgrade panel and no purchase link for 401.

P2 — preserve the sanitized cloud error. Correct. _managed_call flattened every CloudFeatureError to "managed cloud operation failed", so 429, 5xx and a non-consent 409 were indistinguishable. Audited all 19 CloudFeatureError construction sites in cloud_features.py: fixed literals, with the only interpolation being upgrade_url() (operator-controlled) and one template whose substituted value is one of two fixed strings. Messages now pass through, bounded and control-character-checked, placeholder retained as fallback. An AST test now pins that invariant, so a future f-string or reflected response field fails the suite.

P2 — timestamp authoritative billing denials. Correct. entitlement_checked_at never advanced and a repeat denial wrote nothing, so the 15-minute interval never suppressed the next attempt — 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. Tests count real access_for_workspace calls across consecutive requests at the true interval.

P2 — dict | None on Python 3.9 — fixed, but the severity was overstated. The claim was that this makes the module un-importable and that "every Python 3.9 user hits an immediate crash before any update logic runs." That is not the case: scripts/update.py:16 has from __future__ import annotations, so PEP 563 keeps annotations as strings and they are never evaluated at def time. I verified on a real CPython 3.9.25 interpreter — import scripts.update succeeds.

The genuine issue is narrower: typing.get_type_hints() on those three helpers does raise TypeError on 3.9, and dict | None violates the 3.9-compatible-syntax house style (update.py had 0 Optional[...] where v2_api.py has 107). Fixed for those reasons; get_type_hints now resolves on 3.9.

Related and worth a separate issue: the core floor (numpy-only, Python 3.9) job could not have caught this either way. tests/test_client_launch.py:34 has a module-level importorskip("fastapi"), so the whole file — including the five updater tests that need only scripts.update — skips on that job, and scripts/update.py is never imported under 3.9 in CI. Splitting the updater tests into their own file would give the 3.9 floor real coverage of the updater.

Verification

Offline gate: 1,398 passed / 0 failed / 0 errors (1,412 collected), ruff clean, evals at recall@k 1.000. Each fix ships a regression test verified to fail before it.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread engraphis/routes/v2_api.py Outdated
Comment thread engraphis/cloud_session.py Outdated
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>
@Coding-Dev-Tools

Copy link
Copy Markdown
Owner Author

Round 6 — both fixed in e3f9f78

Both were defects in code added earlier in this branch, so they're in scope rather than pre-existing.

P2 — reject caches when the current organization is unknown. Correct; my earlier guard rejected only a mismatch. A fresh bootstrap carries just a refresh credential and a control URL, so _configured_organization_id() is empty until the first refresh names the organization — and in that window a reused state directory served whatever it held. Now requires a positive match. Regression test verified to fail without it ("a cache was trusted before the organization was known").

P2 — serialize billing-denial session updates. Correct, and the more serious of the two. record_billing_denial did an unguarded load-modify-save on the shared session file; a concurrent rotation could have its new credential overwritten with the stale one, and this control plane treats a spent credential as replay and revokes the whole family — so a lapsed subscription would have become a forced reconnect. Read/modify/save now happen inside the same _refresh_lock() access_for_workspace rotates under.

Two things I verified rather than assumed while fixing that one:

  • _refresh_lock is not re-entrant — a nested acquire raises, and record_billing_denial's except Exception would have silently swallowed the denial. It is safe because the lock has already unwound by the time the 402 handler runs; confirmed empirically.
  • A real 402 reaches the handler with its status intact. access_for_workspace has no try around _post_refresh, so _refresh_http_error(402) propagates unrewritten — the == 402 branch genuinely fires rather than being inert.

My first attempt at the lock fix also placed _save() outside the with block, which would have looked correct and fixed nothing; the test asserting "the session was written outside the refresh lock" is what catches that shape.

Verification

1,400 passed / 0 failed / 0 errors (1,414 collected), ruff clean, evals at recall@k 1.000.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

def connect(self):

P1 Badge Reserve timeout for later resolved addresses

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".

@Coding-Dev-Tools
Coding-Dev-Tools merged commit d92e3ee into main Jul 26, 2026
12 checks passed
@Coding-Dev-Tools
Coding-Dev-Tools deleted the launch/payments-licensing-hardening branch July 28, 2026 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant