failproofai 1.0.9-beta.2 → 1.0.10-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ox4w55._.js → [root-of-the-server]__0a2-9ht._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0n6zrjv._.js → _0krq5f0._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1pa3k5k._.js → _1n-mym7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/0s4cko_weh6pi.js +1 -0
- package/.next/standalone/.next/static/chunks/0yclxmfppc_8e.js +1 -0
- package/.next/standalone/.next/static/chunks/{30z-240_lrczt.js → 1-k37ijadiv9h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{28cmd5molgwa3.js → 18reoqya39rov.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2b59i-n9l-4of.js → 2cvvec76j9bwq.js} +1 -1
- package/.next/standalone/.next/static/chunks/2o9-3ff26v4yd.js +1 -0
- package/.next/standalone/.next/static/chunks/3q8vesnwk_r0i.js +1 -0
- package/.next/standalone/.next/static/chunks/3tj1zi4xv-bll.js +1 -0
- package/.next/standalone/.next/static/chunks/3vk6jdkxvb7yk.js +6 -0
- package/.next/standalone/.next/static/chunks/{1srecm9k-z_i1.js → 3z7mx6me_j7d3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2hg_bp8z_vm2g.js → 3ztf2h_fv6_t9.js} +1 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +4 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +29 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +57 -14
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +17 -3
- package/.next/standalone/fp-cloud-cli/tests/test_request_id.py +111 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
- package/.next/standalone/hermes-plugin/README.md +83 -10
- package/.next/standalone/hermes-plugin/__init__.py +762 -28
- package/.next/standalone/hermes-plugin/client.py +4 -2
- package/.next/standalone/hermes-plugin/ledger.py +9 -3
- package/.next/standalone/hermes-plugin/plugin.yaml +2 -2
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +8 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +82 -7
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +23 -4
- package/.next/standalone/sdk/python/tests/test_evaluator_request_id.py +197 -0
- package/.next/standalone/sdk/python/uv.lock +9 -9
- package/.next/standalone/sdk/typescript/CHANGELOG.md +39 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +3 -3
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +16 -113
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +3 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +235 -117
- package/dist/worker.mjs +5 -2
- package/hermes-plugin/README.md +83 -10
- package/hermes-plugin/__init__.py +762 -28
- package/hermes-plugin/client.py +4 -2
- package/hermes-plugin/ledger.py +9 -3
- package/hermes-plugin/plugin.yaml +2 -2
- package/package.json +10 -10
- package/src/hooks/daemon-service.ts +14 -1
- package/src/hooks/integrations.ts +108 -26
- package/src/hooks/safe-config-write.ts +80 -7
- package/src/hooks/types.ts +7 -0
- package/.next/standalone/.next/static/chunks/0h--izcpk__m9.js +0 -1
- package/.next/standalone/.next/static/chunks/2k5vhij_w1g82.js +0 -1
- package/.next/standalone/.next/static/chunks/2o1fy-waqn1he.js +0 -6
- package/.next/standalone/.next/static/chunks/38gpf5tb_rd0m.js +0 -1
- package/.next/standalone/.next/static/chunks/3nyf50dub1vuk.js +0 -1
- package/.next/standalone/.next/static/chunks/42-l25bbfo0ec.js +0 -1
- /package/.next/standalone/.next/static/{D1BjwOkHNPetPo47DhV4_ → OJTzlkhxVSWnwhlF2Q7m3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{D1BjwOkHNPetPo47DhV4_ → OJTzlkhxVSWnwhlF2Q7m3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{D1BjwOkHNPetPo47DhV4_ → OJTzlkhxVSWnwhlF2Q7m3}/_ssgManifest.js +0 -0
|
@@ -98,11 +98,13 @@ def evaluate_policy(
|
|
|
98
98
|
if len(body) > MAX_REQUEST_BYTES:
|
|
99
99
|
raise EvaluationError("policy request exceeds the 1 MiB limit")
|
|
100
100
|
|
|
101
|
+
# One deadline covers connect, send and read: evaluation_timeout_ms is the
|
|
102
|
+
# longest a tool call can wait here. Connect is bounded by both budgets.
|
|
103
|
+
deadline = time.monotonic() + max(evaluation_timeout_ms, 1) / 1000
|
|
101
104
|
try:
|
|
102
105
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
103
|
-
sock.settimeout(max(connect_timeout_ms, 1) / 1000)
|
|
106
|
+
sock.settimeout(min(max(connect_timeout_ms, 1) / 1000, _remaining_timeout(deadline)))
|
|
104
107
|
sock.connect(str(daemon_socket_path()))
|
|
105
|
-
deadline = time.monotonic() + max(evaluation_timeout_ms, 1) / 1000
|
|
106
108
|
sock.settimeout(_remaining_timeout(deadline))
|
|
107
109
|
try:
|
|
108
110
|
sock.sendall(struct.pack(">I", len(body)) + body)
|
|
@@ -23,7 +23,11 @@ class InstructionAction:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class InstructionLedger:
|
|
26
|
-
def __init__(
|
|
26
|
+
def __init__(
|
|
27
|
+
self, path: Path | None, *, ttl_seconds: int = 3600, max_rounds: int = 2
|
|
28
|
+
) -> None:
|
|
29
|
+
# None means the host gave no usable state directory; every use then
|
|
30
|
+
# raises LedgerError, which callers already treat as degraded state.
|
|
27
31
|
self.path = path
|
|
28
32
|
self.ttl_seconds = max(60, int(ttl_seconds))
|
|
29
33
|
self.max_rounds = max(0, int(max_rounds))
|
|
@@ -31,7 +35,9 @@ class InstructionLedger:
|
|
|
31
35
|
self._initialized = False
|
|
32
36
|
|
|
33
37
|
def _connect(self) -> sqlite3.Connection:
|
|
34
|
-
self.path
|
|
38
|
+
if self.path is None:
|
|
39
|
+
raise LedgerError("instruction state directory is unavailable")
|
|
40
|
+
self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
35
41
|
connection = sqlite3.connect(self.path, timeout=0.25, isolation_level=None)
|
|
36
42
|
connection.execute("PRAGMA busy_timeout = 250")
|
|
37
43
|
connection.execute("PRAGMA journal_mode = WAL")
|
|
@@ -70,7 +76,7 @@ class InstructionLedger:
|
|
|
70
76
|
ON instruction_deliveries(profile, session_id, task_id, turn_id, expires_at_ms)
|
|
71
77
|
"""
|
|
72
78
|
)
|
|
73
|
-
except sqlite3.Error as exc:
|
|
79
|
+
except (sqlite3.Error, OSError) as exc:
|
|
74
80
|
raise LedgerError(f"could not initialize instruction state: {exc}") from exc
|
|
75
81
|
self._initialized = True
|
|
76
82
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: failproofai
|
|
2
|
-
version: 1.0.
|
|
2
|
+
version: 1.0.10-beta.1
|
|
3
3
|
description: Real-time FailproofAI policy enforcement for Hermes
|
|
4
4
|
manifest_version: 2
|
|
5
5
|
api_version: 1
|
|
@@ -26,7 +26,7 @@ config_schema:
|
|
|
26
26
|
evaluation_timeout_ms:
|
|
27
27
|
type: int
|
|
28
28
|
default: 12000
|
|
29
|
-
description: Maximum total time
|
|
29
|
+
description: Maximum total time one policy evaluation may take, connect included (capped at 25000).
|
|
30
30
|
instruction_ttl_seconds:
|
|
31
31
|
type: int
|
|
32
32
|
default: 3600
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10-beta.1",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -117,20 +117,20 @@
|
|
|
117
117
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
118
118
|
"vite": "8.0.16",
|
|
119
119
|
"undici": "7.29.1",
|
|
120
|
-
"brace-expansion": "5.0.
|
|
120
|
+
"brace-expansion": "5.0.12",
|
|
121
121
|
"sharp": "0.35.4",
|
|
122
122
|
"browserslist": "4.28.8"
|
|
123
123
|
},
|
|
124
124
|
"optionalDependencies": {
|
|
125
|
-
"@failproofai/failproofaid-linux-x64": "1.0.
|
|
126
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.
|
|
127
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.
|
|
128
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.
|
|
125
|
+
"@failproofai/failproofaid-linux-x64": "1.0.10-beta.1",
|
|
126
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.10-beta.1",
|
|
127
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.10-beta.1",
|
|
128
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.10-beta.1"
|
|
129
129
|
},
|
|
130
130
|
"failproofaidBinaries": {
|
|
131
|
-
"linux-x64": "
|
|
132
|
-
"linux-arm64": "
|
|
133
|
-
"darwin-x64": "
|
|
134
|
-
"darwin-arm64": "
|
|
131
|
+
"linux-x64": "868a4978a66e4407b86dac8d6c8af4ad0b2b355da56983fa5530f5d8b5024098",
|
|
132
|
+
"linux-arm64": "8a4603784e43552ed8cf5ed7a1552721c7216be4fb106ed6851daabd18e19ebe",
|
|
133
|
+
"darwin-x64": "b6a59948ad12c06cedc90ebca95492a357430a2a150b76ceaf98930f8b1279c2",
|
|
134
|
+
"darwin-arm64": "8c14361f7274973bb2cf99135ddb4a5a9cdc8b453a33e653156ba6b68354c417"
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -14,7 +14,12 @@ from failproofai_sdk.evaluator.authoring import (
|
|
|
14
14
|
Metric,
|
|
15
15
|
Score,
|
|
16
16
|
)
|
|
17
|
-
from failproofai_sdk.evaluator.client import
|
|
17
|
+
from failproofai_sdk.evaluator.client import (
|
|
18
|
+
EvaluatorAPIError,
|
|
19
|
+
EvaluatorClient,
|
|
20
|
+
current_request_id,
|
|
21
|
+
request_id_scope,
|
|
22
|
+
)
|
|
18
23
|
from failproofai_sdk.evaluator.protocol import (
|
|
19
24
|
Assignment,
|
|
20
25
|
AssignmentDefinition,
|
|
@@ -70,6 +75,8 @@ __all__ = [
|
|
|
70
75
|
"Evaluator",
|
|
71
76
|
"ManagedCompiler",
|
|
72
77
|
"EvaluatorAPIError",
|
|
78
|
+
"current_request_id",
|
|
79
|
+
"request_id_scope",
|
|
73
80
|
"EvaluatorClient",
|
|
74
81
|
"EvaluatorKind",
|
|
75
82
|
"HeartbeatRequest",
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextvars
|
|
5
6
|
import ipaddress
|
|
6
7
|
import json
|
|
7
8
|
import random
|
|
9
|
+
import re
|
|
8
10
|
import time
|
|
9
|
-
|
|
11
|
+
import uuid
|
|
12
|
+
from collections.abc import Callable, Iterator, Mapping
|
|
13
|
+
from contextlib import contextmanager
|
|
10
14
|
from typing import Any
|
|
11
15
|
from urllib.error import HTTPError, URLError
|
|
12
16
|
from urllib.parse import urljoin, urlsplit
|
|
@@ -42,6 +46,56 @@ from failproofai_sdk.evaluator.protocol import (
|
|
|
42
46
|
_DEFAULT_RESPONSE_LIMIT = 2 * 1024 * 1024
|
|
43
47
|
_RETRYABLE_HTTP_STATUSES = frozenset({429, 502, 503, 504})
|
|
44
48
|
|
|
49
|
+
REQUEST_ID_HEADER = "X-Request-Id"
|
|
50
|
+
# A W3C trace id: the shape the server keeps as-is (it mints its own for
|
|
51
|
+
# anything else, which would break the join between this worker's log line and
|
|
52
|
+
# the server's).
|
|
53
|
+
_TRACE_ID = re.compile(r"^[0-9a-f]{32}$")
|
|
54
|
+
# What we accept back from a server before putting it on an error or a log line.
|
|
55
|
+
_SANE_ECHO = re.compile(r"^[A-Za-z0-9-]{1,64}$")
|
|
56
|
+
|
|
57
|
+
#: The request id the next API call should carry. Unset, every call mints its
|
|
58
|
+
#: own. A caller that already has one — a job it is working on behalf of, or a
|
|
59
|
+
#: host that correlates its own logs — sets it with :func:`request_id_scope`, and
|
|
60
|
+
#: every call inside the scope sends it. ``asyncio.to_thread`` copies context, so
|
|
61
|
+
#: a scope opened in the runtime reaches the client's worker thread.
|
|
62
|
+
current_request_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
63
|
+
"failproofai_evaluator_request_id", default=None
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def new_request_id() -> str:
|
|
68
|
+
return uuid.uuid4().hex
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@contextmanager
|
|
72
|
+
def request_id_scope(request_id: str | None = None) -> Iterator[str]:
|
|
73
|
+
"""Send ``request_id`` (or a fresh one) on every API call inside the block.
|
|
74
|
+
|
|
75
|
+
An id that is not a trace id (32 lowercase hex) is replaced with a fresh one
|
|
76
|
+
rather than sent, so a caller can never put free text on the wire.
|
|
77
|
+
"""
|
|
78
|
+
value = request_id if request_id and _TRACE_ID.match(request_id) else new_request_id()
|
|
79
|
+
token = current_request_id.set(value)
|
|
80
|
+
try:
|
|
81
|
+
yield value
|
|
82
|
+
finally:
|
|
83
|
+
current_request_id.reset(token)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _request_id_for_call() -> str:
|
|
87
|
+
current = current_request_id.get()
|
|
88
|
+
return current if current and _TRACE_ID.match(current) else new_request_id()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _echoed_request_id(headers: Any) -> str | None:
|
|
92
|
+
"""The server's ``x-request-id`` from a response's headers, when sane."""
|
|
93
|
+
if headers is None:
|
|
94
|
+
return None
|
|
95
|
+
value = headers.get(REQUEST_ID_HEADER) or headers.get(REQUEST_ID_HEADER.lower())
|
|
96
|
+
value = (value or "").strip()
|
|
97
|
+
return value if _SANE_ECHO.match(value) else None
|
|
98
|
+
|
|
45
99
|
|
|
46
100
|
class _RejectRedirects(HTTPRedirectHandler):
|
|
47
101
|
def redirect_request(self, request, file_pointer, code, message, headers, new_url):
|
|
@@ -222,6 +276,11 @@ class EvaluatorClient:
|
|
|
222
276
|
separators=(",", ":"),
|
|
223
277
|
).encode("utf-8")
|
|
224
278
|
request_headers["Content-Type"] = "application/json"
|
|
279
|
+
# One id per call, shared by its retries: a retried call is one logical
|
|
280
|
+
# request, and the server's log lines for every attempt then group under
|
|
281
|
+
# one id. The caller's scope id wins (see `current_request_id`).
|
|
282
|
+
request_id = _request_id_for_call()
|
|
283
|
+
request_headers[REQUEST_ID_HEADER] = request_id
|
|
225
284
|
if headers:
|
|
226
285
|
request_headers.update(headers)
|
|
227
286
|
|
|
@@ -232,11 +291,18 @@ class EvaluatorClient:
|
|
|
232
291
|
)
|
|
233
292
|
try:
|
|
234
293
|
with self._opener(request, timeout=self._timeout_seconds) as response:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
294
|
+
try:
|
|
295
|
+
return self._decode(
|
|
296
|
+
response.read(response_limit + 1), response_limit
|
|
297
|
+
)
|
|
298
|
+
except EvaluatorAPIError as error:
|
|
299
|
+
error.request_id = (
|
|
300
|
+
_echoed_request_id(getattr(response, "headers", None))
|
|
301
|
+
or request_id
|
|
302
|
+
)
|
|
303
|
+
raise
|
|
238
304
|
except HTTPError as error:
|
|
239
|
-
api_error = self._http_error(error, response_limit)
|
|
305
|
+
api_error = self._http_error(error, response_limit, request_id)
|
|
240
306
|
if attempt + 1 == attempts or not api_error.retryable:
|
|
241
307
|
raise api_error from error
|
|
242
308
|
except (URLError, TimeoutError, OSError) as error:
|
|
@@ -246,6 +312,8 @@ class EvaluatorClient:
|
|
|
246
312
|
code="transport_error",
|
|
247
313
|
message=str(error),
|
|
248
314
|
retryable=True,
|
|
315
|
+
# Never answered: ours is the only id there is.
|
|
316
|
+
request_id=request_id,
|
|
249
317
|
) from error
|
|
250
318
|
# Jitter is scheduling noise, not a security decision.
|
|
251
319
|
self._sleeper(random.uniform(0, min(0.25 * (2**attempt), 2.0))) # nosec B311
|
|
@@ -279,7 +347,13 @@ class EvaluatorClient:
|
|
|
279
347
|
return value
|
|
280
348
|
|
|
281
349
|
@classmethod
|
|
282
|
-
def _http_error(
|
|
350
|
+
def _http_error(
|
|
351
|
+
cls, error: HTTPError, limit: int, sent_request_id: str | None = None
|
|
352
|
+
) -> EvaluatorAPIError:
|
|
353
|
+
# The server's id for this request: its logs are keyed on it. The
|
|
354
|
+
# envelope's own field first, then the response header, then ours — a
|
|
355
|
+
# proxy that answered in the server's place echoes nothing.
|
|
356
|
+
echoed = _echoed_request_id(error.headers) or sent_request_id
|
|
283
357
|
raw = error.read(limit + 1)
|
|
284
358
|
try:
|
|
285
359
|
response = ErrorResponse.from_wire(cls._decode(raw, limit))
|
|
@@ -289,11 +363,12 @@ class EvaluatorClient:
|
|
|
289
363
|
code="http_error",
|
|
290
364
|
message=f"server returned HTTP {error.code}",
|
|
291
365
|
retryable=error.code in _RETRYABLE_HTTP_STATUSES,
|
|
366
|
+
request_id=echoed,
|
|
292
367
|
)
|
|
293
368
|
return EvaluatorAPIError(
|
|
294
369
|
status=error.code,
|
|
295
370
|
code=response.error.code,
|
|
296
371
|
message=response.error.message,
|
|
297
372
|
retryable=response.error.retryable,
|
|
298
|
-
request_id=response.error.request_id,
|
|
373
|
+
request_id=response.error.request_id or echoed,
|
|
299
374
|
)
|
|
@@ -289,9 +289,17 @@ class WorkerRuntime:
|
|
|
289
289
|
)
|
|
290
290
|
except EvaluatorAPIError as error:
|
|
291
291
|
self._increment("claim_failures")
|
|
292
|
+
# The id rides in the message as well as `extra`: a plain
|
|
293
|
+
# `logging.basicConfig` formatter drops extras, and this is
|
|
294
|
+
# the id that finds the server's side of the failure.
|
|
292
295
|
logger.warning(
|
|
293
|
-
"evaluator claim failed",
|
|
294
|
-
|
|
296
|
+
"evaluator claim failed (request_id=%s)",
|
|
297
|
+
error.request_id,
|
|
298
|
+
extra={
|
|
299
|
+
"code": error.code,
|
|
300
|
+
"retryable": error.retryable,
|
|
301
|
+
"request_id": error.request_id,
|
|
302
|
+
},
|
|
295
303
|
)
|
|
296
304
|
if not error.retryable:
|
|
297
305
|
raise
|
|
@@ -372,8 +380,11 @@ class WorkerRuntime:
|
|
|
372
380
|
# transcript that can never shrink. Log and return; the server
|
|
373
381
|
# terminalizes the assignment as `too_large`.
|
|
374
382
|
logger.warning(
|
|
375
|
-
"assignment %s transcript is too large to evaluate; skipping"
|
|
383
|
+
"assignment %s transcript is too large to evaluate; skipping "
|
|
384
|
+
"(request_id=%s)",
|
|
376
385
|
assignment.assignment_id,
|
|
386
|
+
error.request_id,
|
|
387
|
+
extra={"request_id": error.request_id},
|
|
377
388
|
)
|
|
378
389
|
self._increment("transcripts_too_large")
|
|
379
390
|
return
|
|
@@ -796,10 +807,12 @@ class WorkerRuntime:
|
|
|
796
807
|
task.cancel()
|
|
797
808
|
return
|
|
798
809
|
logger.warning(
|
|
799
|
-
"evaluator heartbeat failed",
|
|
810
|
+
"evaluator heartbeat failed (request_id=%s)",
|
|
811
|
+
error.request_id,
|
|
800
812
|
extra={
|
|
801
813
|
"assignment_id": assignment.assignment_id,
|
|
802
814
|
"code": error.code,
|
|
815
|
+
"request_id": error.request_id,
|
|
803
816
|
},
|
|
804
817
|
)
|
|
805
818
|
self._increment("heartbeat_failures")
|
|
@@ -924,6 +937,12 @@ class WorkerRuntime:
|
|
|
924
937
|
task.result()
|
|
925
938
|
except asyncio.CancelledError:
|
|
926
939
|
pass
|
|
940
|
+
except EvaluatorAPIError as error:
|
|
941
|
+
logger.exception(
|
|
942
|
+
"evaluator assignment failed (request_id=%s)",
|
|
943
|
+
error.request_id,
|
|
944
|
+
extra={"code": error.code, "request_id": error.request_id},
|
|
945
|
+
)
|
|
927
946
|
except Exception:
|
|
928
947
|
logger.exception("evaluator assignment failed")
|
|
929
948
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Every evaluator API call carries a request id, and every failure names one.
|
|
2
|
+
|
|
3
|
+
The server logs each worker call under its `x-request-id`. A worker log line
|
|
4
|
+
that says `request_id=4bf92f35…` is then one query away from the server's side
|
|
5
|
+
of the same failure — but only if the worker sent an id the server keeps (a
|
|
6
|
+
32-hex trace id), and only if the error carries the id the server logged.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import io
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
from email.message import Message
|
|
17
|
+
from urllib.error import HTTPError, URLError
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
from failproofai_sdk.evaluator import (
|
|
22
|
+
ClaimRequest,
|
|
23
|
+
EvaluatorAPIError,
|
|
24
|
+
EvaluatorClient,
|
|
25
|
+
current_request_id,
|
|
26
|
+
request_id_scope,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
TRACE_ID = re.compile(r"^[0-9a-f]{32}$")
|
|
30
|
+
SERVER_ID = "4bf92f3577b34da6a3ce929d0e0e4736"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Response:
|
|
34
|
+
def __init__(self, body, headers=None):
|
|
35
|
+
self.body = json.dumps(body).encode() if not isinstance(body, bytes) else body
|
|
36
|
+
self.headers = headers or {}
|
|
37
|
+
|
|
38
|
+
def __enter__(self):
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def __exit__(self, *args):
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def read(self, amount):
|
|
45
|
+
return self.body[:amount]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _client(opener):
|
|
49
|
+
return EvaluatorClient(
|
|
50
|
+
base_url="https://cloud.example/",
|
|
51
|
+
credential="secret",
|
|
52
|
+
opener=opener,
|
|
53
|
+
sleeper=lambda _: None,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _claim(client):
|
|
58
|
+
return client.claim(ClaimRequest("worker", "sha256:x", 1, 20))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _headers(**values):
|
|
62
|
+
message = Message()
|
|
63
|
+
for key, value in values.items():
|
|
64
|
+
message[key.replace("_", "-")] = value
|
|
65
|
+
return message
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_every_call_sends_a_fresh_trace_id():
|
|
69
|
+
sent = []
|
|
70
|
+
|
|
71
|
+
def opener(request, timeout):
|
|
72
|
+
sent.append(request.get_header("X-request-id"))
|
|
73
|
+
return Response({"assignments": [], "lease_duration_seconds": 30})
|
|
74
|
+
|
|
75
|
+
client = _client(opener)
|
|
76
|
+
for _ in range(2):
|
|
77
|
+
try:
|
|
78
|
+
_claim(client)
|
|
79
|
+
except Exception: # noqa: BLE001 - the response shape is not under test here
|
|
80
|
+
pass
|
|
81
|
+
assert len(sent) == 2
|
|
82
|
+
assert all(TRACE_ID.match(i or "") for i in sent), sent
|
|
83
|
+
assert sent[0] != sent[1]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_retries_of_one_call_share_its_id():
|
|
87
|
+
sent = []
|
|
88
|
+
|
|
89
|
+
def opener(request, timeout):
|
|
90
|
+
sent.append(request.get_header("X-request-id"))
|
|
91
|
+
raise URLError("reset")
|
|
92
|
+
|
|
93
|
+
with pytest.raises(EvaluatorAPIError) as caught:
|
|
94
|
+
# heartbeat retries; claim deliberately does not
|
|
95
|
+
_client(opener)._json("POST", "/v1/evaluator/runs/heartbeat", None, retry=True)
|
|
96
|
+
assert len(sent) == 4
|
|
97
|
+
assert len(set(sent)) == 1
|
|
98
|
+
# A call that never got an answer names the id it sent.
|
|
99
|
+
assert caught.value.request_id == sent[0]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_a_caller_scope_id_is_honoured():
|
|
103
|
+
sent = []
|
|
104
|
+
|
|
105
|
+
def opener(request, timeout):
|
|
106
|
+
sent.append(request.get_header("X-request-id"))
|
|
107
|
+
raise URLError("offline")
|
|
108
|
+
|
|
109
|
+
with request_id_scope(SERVER_ID) as scoped:
|
|
110
|
+
assert scoped == SERVER_ID
|
|
111
|
+
with pytest.raises(EvaluatorAPIError):
|
|
112
|
+
_claim(_client(opener))
|
|
113
|
+
assert sent == [SERVER_ID]
|
|
114
|
+
assert current_request_id.get() is None, "the scope must not leak"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_a_scope_reaches_the_thread_the_runtime_calls_the_client_on():
|
|
118
|
+
# The runtime calls the client via asyncio.to_thread, which copies context.
|
|
119
|
+
sent = []
|
|
120
|
+
|
|
121
|
+
def opener(request, timeout):
|
|
122
|
+
sent.append(request.get_header("X-request-id"))
|
|
123
|
+
raise URLError("offline")
|
|
124
|
+
|
|
125
|
+
async def main():
|
|
126
|
+
with request_id_scope(SERVER_ID):
|
|
127
|
+
with pytest.raises(EvaluatorAPIError):
|
|
128
|
+
await asyncio.to_thread(_claim, _client(opener))
|
|
129
|
+
|
|
130
|
+
asyncio.run(main())
|
|
131
|
+
assert sent == [SERVER_ID]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_a_scope_with_free_text_is_replaced_not_sent():
|
|
135
|
+
with request_id_scope("evil\nheader") as scoped:
|
|
136
|
+
assert TRACE_ID.match(scoped)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_an_http_error_carries_the_servers_echoed_id():
|
|
140
|
+
def opener(request, timeout):
|
|
141
|
+
raise HTTPError(
|
|
142
|
+
request.full_url, 503, "Unavailable", _headers(x_request_id=SERVER_ID), io.BytesIO(b"")
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
with pytest.raises(EvaluatorAPIError) as caught:
|
|
146
|
+
_claim(_client(opener))
|
|
147
|
+
assert caught.value.status == 503
|
|
148
|
+
assert caught.value.request_id == SERVER_ID
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_without_an_echo_the_error_names_the_id_it_sent():
|
|
152
|
+
sent = []
|
|
153
|
+
|
|
154
|
+
def opener(request, timeout):
|
|
155
|
+
sent.append(request.get_header("X-request-id"))
|
|
156
|
+
raise HTTPError(request.full_url, 502, "Bad Gateway", _headers(), io.BytesIO(b"<html>"))
|
|
157
|
+
|
|
158
|
+
with pytest.raises(EvaluatorAPIError) as caught:
|
|
159
|
+
_claim(_client(opener))
|
|
160
|
+
assert caught.value.request_id == sent[0]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_a_malformed_success_body_still_names_an_id():
|
|
164
|
+
def opener(request, timeout):
|
|
165
|
+
return Response(b"<html>", headers=_headers(x_request_id=SERVER_ID))
|
|
166
|
+
|
|
167
|
+
with pytest.raises(EvaluatorAPIError) as caught:
|
|
168
|
+
_claim(_client(opener))
|
|
169
|
+
assert caught.value.code == "invalid_response"
|
|
170
|
+
assert caught.value.request_id == SERVER_ID
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_the_runtimes_claim_failure_line_names_the_request_id(caplog):
|
|
174
|
+
# Drive the real runtime path: a claim the server refuses must leave a warn
|
|
175
|
+
# line whose MESSAGE carries the id (a basicConfig formatter drops extras).
|
|
176
|
+
from tests.test_evaluator_runtime import FakeClient, _runtime
|
|
177
|
+
|
|
178
|
+
from failproofai_sdk.evaluator import Evaluator
|
|
179
|
+
|
|
180
|
+
class RejectedClaimClient(FakeClient):
|
|
181
|
+
def claim(self, request):
|
|
182
|
+
raise EvaluatorAPIError(
|
|
183
|
+
status=409,
|
|
184
|
+
code="catalog_mismatch",
|
|
185
|
+
message="register again",
|
|
186
|
+
retryable=False,
|
|
187
|
+
request_id=SERVER_ID,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
runtime = _runtime(Evaluator(name="test", version="1"), RejectedClaimClient())
|
|
191
|
+
with caplog.at_level(logging.WARNING, logger="failproofai_sdk.evaluator"):
|
|
192
|
+
with pytest.raises(EvaluatorAPIError):
|
|
193
|
+
asyncio.run(runtime.run_forever())
|
|
194
|
+
lines = [r for r in caplog.records if "claim failed" in r.getMessage()]
|
|
195
|
+
assert lines, [r.getMessage() for r in caplog.records]
|
|
196
|
+
assert SERVER_ID in lines[0].getMessage()
|
|
197
|
+
assert lines[0].request_id == SERVER_ID
|
|
@@ -2934,11 +2934,11 @@ wheels = [
|
|
|
2934
2934
|
|
|
2935
2935
|
[[package]]
|
|
2936
2936
|
name = "oauthlib"
|
|
2937
|
-
version = "
|
|
2937
|
+
version = "4.0.0"
|
|
2938
2938
|
source = { registry = "https://pypi.org/simple" }
|
|
2939
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
2939
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/a1bcc8ba112a627f8ffbdc212a78ce18d3ac07e91a5ca65d27918eee25a1/oauthlib-4.0.0.tar.gz", hash = "sha256:efb274799819440f95b4ab3b818869f1ce9ae26c5beacba0201d1a1b76b54f86", size = 187232, upload-time = "2026-09-28T06:01:18.77Z" }
|
|
2940
2940
|
wheels = [
|
|
2941
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
2941
|
+
{ url = "https://files.pythonhosted.org/packages/d9/f4/78229a1066068ca14fc60fb26cf7381cabe4382261392b90e5f9552722d4/oauthlib-4.0.0-py3-none-any.whl", hash = "sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd", size = 159715, upload-time = "2026-09-28T06:01:17.008Z" },
|
|
2942
2942
|
]
|
|
2943
2943
|
|
|
2944
2944
|
[[package]]
|
|
@@ -4082,14 +4082,14 @@ wheels = [
|
|
|
4082
4082
|
|
|
4083
4083
|
[[package]]
|
|
4084
4084
|
name = "pyjwt"
|
|
4085
|
-
version = "2.
|
|
4085
|
+
version = "2.15.1"
|
|
4086
4086
|
source = { registry = "https://pypi.org/simple" }
|
|
4087
4087
|
dependencies = [
|
|
4088
4088
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
|
4089
4089
|
]
|
|
4090
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
4090
|
+
sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" }
|
|
4091
4091
|
wheels = [
|
|
4092
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
4092
|
+
{ url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" },
|
|
4093
4093
|
]
|
|
4094
4094
|
|
|
4095
4095
|
[package.optional-dependencies]
|
|
@@ -5120,11 +5120,11 @@ wheels = [
|
|
|
5120
5120
|
|
|
5121
5121
|
[[package]]
|
|
5122
5122
|
name = "urllib3"
|
|
5123
|
-
version = "2.
|
|
5123
|
+
version = "2.8.0"
|
|
5124
5124
|
source = { registry = "https://pypi.org/simple" }
|
|
5125
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
5125
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
|
|
5126
5126
|
wheels = [
|
|
5127
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
5127
|
+
{ url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
|
|
5128
5128
|
]
|
|
5129
5129
|
|
|
5130
5130
|
[[package]]
|
|
@@ -45,6 +45,45 @@ section is missing or empty is refused before anything is built.
|
|
|
45
45
|
`ai` stays at `7.0.111` in `nextjs` so the fixture still matches the `ai-7` twin
|
|
46
46
|
whose trace `nextjs.test.ts` compares each route against. (#838)
|
|
47
47
|
|
|
48
|
+
- **Two more fixture advisories clear, and again no fixture leaves its pinned
|
|
49
|
+
major.** Both were reachable the whole time; nothing had looked. `ai-5` pinned
|
|
50
|
+
`undici` at `7.29.0` through the override added in #837, and GHSA-3wwx-pv8p-q78v
|
|
51
|
+
(medium) is fixed in `7.29.1` — the root `package.json`'s own `undici` override
|
|
52
|
+
moved there, the fixture's did not. `mastra-0` carried **eleven** copies of
|
|
53
|
+
`@ai-sdk/provider-utils`, which split in two: nine parents on that fixture's 5.x
|
|
54
|
+
side (`ai-v5`, `@ai-sdk/gateway`, `@ai-sdk/openai-v5` and six siblings) pinned
|
|
55
|
+
exact `3.0.10`–`3.0.17`, every one below the `3.0.28` that fixes
|
|
56
|
+
GHSA-866g-f22w-33x8, while the other two are the `2.2.8` that ships *inside*
|
|
57
|
+
`ai@4.3.19` under `@mastra/core` and `@mastra/memory`. The accepted row above
|
|
58
|
+
describes only that second pair, so the 3.x half was never covered by it — a
|
|
59
|
+
plain `"@ai-sdk/provider-utils": "3.0.28"` would have forced both halves and
|
|
60
|
+
broken the pin. The entry is **spec-scoped** instead — `"@ai-sdk/provider-utils@3"`,
|
|
61
|
+
which npm applies only where the requested range already resolves inside 3.x —
|
|
62
|
+
so the 3.x copies collapse to one patched copy, the two `2.2.8` copies are
|
|
63
|
+
untouched, and `@mastra/core@0.24.9`, `@mastra/mcp@0.14.5`,
|
|
64
|
+
`@mastra/memory@0.15.13` and `ai@4.3.19` all resolve exactly as before. That
|
|
65
|
+
dedupe is most of the ~110 deleted lockfile lines.
|
|
66
|
+
|
|
67
|
+
**One of the nine hid behind an npm alias**, and that is the part worth
|
|
68
|
+
remembering. `@mastra/core@0.24.9` does not ask for `@ai-sdk/provider-utils`
|
|
69
|
+
by name — it asks for `"@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.12"`,
|
|
70
|
+
one of ten `-v5` aliases in that lockfile. npm keys `overrides` on the name the
|
|
71
|
+
**dependent requests**, not on the name that ends up installed, so
|
|
72
|
+
`"@ai-sdk/provider-utils@3"` never applied to it and an affected `3.0.12` stayed
|
|
73
|
+
in the tree looking, from every path-keyed check, exactly like a package that had
|
|
74
|
+
been patched. It needed its own alias-keyed entry,
|
|
75
|
+
`"@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.28"`. Verifying an
|
|
76
|
+
override by globbing lockfile paths for the real package name is what missed it;
|
|
77
|
+
`npm audit` names the alias directly. The other nine aliases are unaffected —
|
|
78
|
+
`ai-v5` resolves `5.0.60` and `5.0.97`, both above GHSA-rwvc-j5jr-mgvh's `5.0.52`
|
|
79
|
+
floor. Dependabot had reached the same two alerts by moving
|
|
80
|
+
`ai-4` to `ai@5.0.52`, `ai-5` to `ai@7.0.122` and `mastra-0` to
|
|
81
|
+
`@mastra/core@1.71.0` (#867) — the third time in as many days that its fix for a
|
|
82
|
+
fixture advisory was the one `SECURITY.md` forbids, and it failed the same
|
|
83
|
+
`ERESOLVE` on the same `@mastra/mcp` peer as #838. `ai-4` needed no change: both
|
|
84
|
+
advisories still open against it are the accepted kind, and the table above
|
|
85
|
+
already carries them. (#869)
|
|
86
|
+
|
|
48
87
|
## 0.0.1-beta.0 — 2026-09-23
|
|
49
88
|
|
|
50
89
|
### Added
|
|
@@ -128,9 +128,9 @@
|
|
|
128
128
|
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
|
129
129
|
},
|
|
130
130
|
"node_modules/undici": {
|
|
131
|
-
"version": "7.29.
|
|
132
|
-
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.
|
|
133
|
-
"integrity": "sha512-
|
|
131
|
+
"version": "7.29.1",
|
|
132
|
+
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
|
|
133
|
+
"integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
|
|
134
134
|
"license": "MIT",
|
|
135
135
|
"engines": {
|
|
136
136
|
"node": ">=20.18.1"
|