failproofai 1.0.9 → 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.
Files changed (131) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +14 -14
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  54. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ec94jt._.js → [root-of-the-server]__0a2-9ht._.js} +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_0xq-p5o._.js → _0krq5f0._.js} +3 -3
  75. package/.next/standalone/.next/server/chunks/ssr/{_1g7s7di._.js → _1n-mym7._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  83. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  84. package/.next/standalone/.next/server/pages/404.html +1 -1
  85. package/.next/standalone/.next/server/pages/500.html +1 -1
  86. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  87. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  88. package/.next/standalone/.next/static/chunks/{28z6u4u7hdrnd.js → 0s4cko_weh6pi.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/0yclxmfppc_8e.js +1 -0
  90. package/.next/standalone/.next/static/chunks/{1t6tmwe_1myeo.js → 1-k37ijadiv9h.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/{2dj6xq9mueqb6.js → 18reoqya39rov.js} +1 -1
  92. package/.next/standalone/.next/static/chunks/{0zlixskj11-yh.js → 2cvvec76j9bwq.js} +1 -1
  93. package/.next/standalone/.next/static/chunks/2o9-3ff26v4yd.js +1 -0
  94. package/.next/standalone/.next/static/chunks/{3fbsuyflx-2qi.js → 3q8vesnwk_r0i.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/3tj1zi4xv-bll.js +1 -0
  96. package/.next/standalone/.next/static/chunks/{3jffngwvgxm5q.js → 3vk6jdkxvb7yk.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{2lczelmvvc2yg.js → 3z7mx6me_j7d3.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{2hg_bp8z_vm2g.js → 3ztf2h_fv6_t9.js} +1 -1
  99. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +4 -0
  100. package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +29 -4
  101. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +57 -14
  102. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +17 -3
  103. package/.next/standalone/fp-cloud-cli/tests/test_request_id.py +111 -0
  104. package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
  105. package/.next/standalone/hermes-plugin/README.md +83 -10
  106. package/.next/standalone/hermes-plugin/__init__.py +762 -28
  107. package/.next/standalone/hermes-plugin/client.py +4 -2
  108. package/.next/standalone/hermes-plugin/ledger.py +9 -3
  109. package/.next/standalone/hermes-plugin/plugin.yaml +2 -2
  110. package/.next/standalone/package.json +10 -10
  111. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +8 -1
  112. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +82 -7
  113. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +23 -4
  114. package/.next/standalone/sdk/python/tests/test_evaluator_request_id.py +197 -0
  115. package/.next/standalone/sdk/python/uv.lock +9 -9
  116. package/.next/standalone/server.js +1 -1
  117. package/dist/cli.mjs +15 -12
  118. package/dist/worker.mjs +5 -2
  119. package/hermes-plugin/README.md +83 -10
  120. package/hermes-plugin/__init__.py +762 -28
  121. package/hermes-plugin/client.py +4 -2
  122. package/hermes-plugin/ledger.py +9 -3
  123. package/hermes-plugin/plugin.yaml +2 -2
  124. package/package.json +10 -10
  125. package/src/hooks/types.ts +7 -0
  126. package/.next/standalone/.next/static/chunks/03madt33xbsx2.js +0 -1
  127. package/.next/standalone/.next/static/chunks/0w8_n30wmnorq.js +0 -1
  128. package/.next/standalone/.next/static/chunks/1-n1o5wh4wfy6.js +0 -1
  129. /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → OJTzlkhxVSWnwhlF2Q7m3}/_buildManifest.js +0 -0
  130. /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → OJTzlkhxVSWnwhlF2Q7m3}/_clientMiddlewareManifest.js +0 -0
  131. /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → 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__(self, path: Path, *, ttl_seconds: int = 3600, max_rounds: int = 2) -> None:
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.parent.mkdir(parents=True, exist_ok=True)
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.6-beta.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 allowed for one policy evaluation.
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.9",
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.9",
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.9",
126
- "@failproofai/failproofaid-linux-arm64": "1.0.9",
127
- "@failproofai/failproofaid-darwin-x64": "1.0.9",
128
- "@failproofai/failproofaid-darwin-arm64": "1.0.9"
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": "c406199fbd5682ee84eb3bdd679fe48d1f78a26e97710f05bcfe42771e33ca4a",
132
- "linux-arm64": "8ec339035013aaf069bd882660b89eefde137a5fb3c66bed9c65bfbfa1afaa19",
133
- "darwin-x64": "fdf77e2432131ff6081769555c0a87d709d3380c78bc33de92743694dd20a83b",
134
- "darwin-arm64": "a85e7e2af98fbcc09b6dfdd7fba8289d309757eab6482770ca64d3eeb81407d5"
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 EvaluatorAPIError, EvaluatorClient
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
- from collections.abc import Callable, Mapping
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
- return self._decode(
236
- response.read(response_limit + 1), response_limit
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(cls, error: HTTPError, limit: int) -> EvaluatorAPIError:
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
- extra={"code": error.code, "retryable": error.retryable},
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 = "3.3.1"
2937
+ version = "4.0.0"
2938
2938
  source = { registry = "https://pypi.org/simple" }
2939
- sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
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/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
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.13.0"
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/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
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/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
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.7.0"
5123
+ version = "2.8.0"
5124
5124
  source = { registry = "https://pypi.org/simple" }
5125
- sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
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/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
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]]
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
9
9
  const hostname = process.env.HOSTNAME || '0.0.0.0'
10
10
 
11
11
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
12
- const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.9"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"instrumentationClientInject":[],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","enablePrerenderSourceMaps":true,"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":true,"coldCacheBadge":false,"devValidationWorker":true,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"dynamicOnHover":false,"useOffline":false,"varyParams":true,"optimisticRouting":true,"instrumentationClientRouterTransitionEvents":false,"prefetchInlining":{"maxSize":2048,"maxBundleSize":10240},"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptOperationCache":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"devMemoryThresholdRestart":true,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"requestInsights":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"useTypeScriptCli":true,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":true,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"serverComponentsHmrCancellation":false,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":true,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","turbopackMemoryEvictionMode":"auto","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"useCacheTimeout":54,"instantInsights":{"validationLevel":"warning"},"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"repoRoot":"/home/runner/work/failproofai/failproofai","distDirRoot":".next"}
12
+ const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.10-beta.1"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"instrumentationClientInject":[],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","enablePrerenderSourceMaps":true,"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":true,"coldCacheBadge":false,"devValidationWorker":true,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"dynamicOnHover":false,"useOffline":false,"varyParams":true,"optimisticRouting":true,"instrumentationClientRouterTransitionEvents":false,"prefetchInlining":{"maxSize":2048,"maxBundleSize":10240},"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptOperationCache":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"devMemoryThresholdRestart":true,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"requestInsights":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"useTypeScriptCli":true,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":true,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"serverComponentsHmrCancellation":false,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":true,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","turbopackMemoryEvictionMode":"auto","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"useCacheTimeout":54,"instantInsights":{"validationLevel":"warning"},"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"repoRoot":"/home/runner/work/failproofai/failproofai","distDirRoot":".next"}
13
13
 
14
14
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
15
15