diff --git a/CHANGELOG.md b/CHANGELOG.md index 22edb18..fee5355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,10 @@ Public 1.0.1 client reliability release. - 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. +- Default Pro and Team upgrade links now target the live authenticated account portal rather + than the retired Team dashboard host. +- Hosted endpoint validation now fails closed when DNS resolution cannot establish that a + configured destination is public, preventing unresolved hosts from bypassing the SSRF guard. - 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 @@ -73,6 +77,7 @@ Public 1.0.1 client reliability release. - Entity extraction and dashboard asset migration avoid adversarial regular-expression backtracking. CodeQL now disables pull-request diff-informed analysis and CI fails on every raw SARIF result, including pre-existing and source-suppressed results. +- The documented grounded-recall evaluation prints with the default Windows console encoding. ## [1.0.0] - 2026-07-23 diff --git a/engraphis/hosted_client.py b/engraphis/hosted_client.py index c8ead70..9db914b 100644 --- a/engraphis/hosted_client.py +++ b/engraphis/hosted_client.py @@ -17,7 +17,10 @@ TRIAL_SECONDS = 3 * 24 * 60 * 60 MAX_LOCAL_WRITE_GRACE_SECONDS = 24 * 60 * 60 -DEFAULT_CLOUD_URL = "https://team.engraphis.com" +# The hosted dashboard and the commercial account portal are separate surfaces. +# Upgrade/connect actions must land on the authenticated control-plane portal; the +# dashboard host does not serve its own ``/account`` route. +DEFAULT_CLOUD_URL = "https://api.engraphis.com/account" _REQUIRED_PLAN = { "analytics": "pro", @@ -115,5 +118,7 @@ def validate_cloud_base_url(value: str) -> str: "cloud service URL must not target private/reserved IP ranges" ) except (socket.gaierror, OSError): - pass + raise ValueError( + "cloud service URL could not be resolved" + ) from None return urlunsplit((scheme, parts.netloc, parts.path.rstrip("/"), "", "")) diff --git a/eval/grounded.py b/eval/grounded.py index e0aa146..a22d5f5 100644 --- a/eval/grounded.py +++ b/eval/grounded.py @@ -66,9 +66,11 @@ def run() -> dict: def main() -> None: r = run() print("\nEngraphis grounded-recall eval (deterministic embedder)") - print(f" answerable → grounded : {r['answer_rate']:.3f} " + # Keep the documented offline gate usable from the Windows console's default + # cp1252 encoding, which cannot emit a Unicode arrow. + print(f" answerable -> grounded : {r['answer_rate']:.3f} " f"({r['grounded_hits']}/{r['n_answerable']})") - print(f" off-topic → abstained : {r['abstain_rate']:.3f} " + print(f" off-topic -> abstained : {r['abstain_rate']:.3f} " f"({r['abstain_hits']}/{r['n_unanswerable']})") print(f" decision accuracy : {r['accuracy']:.3f} " f"({r['grounded_hits'] + r['abstain_hits']}/{r['n_answerable'] + r['n_unanswerable']})\n") diff --git a/tests/test_eval_grounded.py b/tests/test_eval_grounded.py new file mode 100644 index 0000000..4f3cf19 --- /dev/null +++ b/tests/test_eval_grounded.py @@ -0,0 +1,9 @@ +"""Regression coverage for the documented offline grounded-recall gate.""" + +from eval import grounded + + +def test_grounded_eval_report_is_windows_console_safe(capsys): + """The release command must work with Windows' default cp1252 stdout.""" + grounded.main() + capsys.readouterr().out.encode("cp1252") diff --git a/tests/test_hosted_client.py b/tests/test_hosted_client.py index edc9402..1e3dbaf 100644 --- a/tests/test_hosted_client.py +++ b/tests/test_hosted_client.py @@ -17,13 +17,18 @@ def test_upgrade_urls_are_hosted_metadata_only(monkeypatch): monkeypatch.delenv("ENGRAPHIS_TEAM_UPGRADE_URL", raising=False) monkeypatch.delenv("ENGRAPHIS_CLOUD_URL", raising=False) - assert hosted_client.upgrade_url("pro") == "https://team.engraphis.com" - assert hosted_client.upgrade_url("team") == "https://team.engraphis.com" + assert hosted_client.upgrade_url("pro") == "https://api.engraphis.com/account" + assert hosted_client.upgrade_url("team") == "https://api.engraphis.com/account" assert hosted_client.required_plan("sync") == "pro" assert hosted_client.required_plan("team") == "team" -def test_cloud_url_validation_requires_safe_remote_https(): +def test_cloud_url_validation_requires_safe_remote_https(monkeypatch): + monkeypatch.setattr( + hosted_client.socket, + "getaddrinfo", + lambda *a, **k: [(2, 1, 6, "", ("1.2.3.4", 0))], + ) assert hosted_client.validate_cloud_base_url("http://127.0.0.1:8700/") == ( "http://127.0.0.1:8700" ) @@ -40,6 +45,16 @@ def test_cloud_url_validation_requires_safe_remote_https(): hosted_client.validate_cloud_base_url(invalid) +def test_cloud_url_validation_rejects_unresolvable_hosts(monkeypatch): + monkeypatch.setattr( + hosted_client.socket, + "getaddrinfo", + lambda *a, **k: (_ for _ in ()).throw(hosted_client.socket.gaierror), + ) + with pytest.raises(ValueError, match="could not be resolved"): + hosted_client.validate_cloud_base_url("https://unresolvable.example/") + + def test_licensing_facade_exposes_no_local_entitlement_engine(): assert licensing.TRIAL_DAYS == 3 assert licensing.production_warnings() == []