Skip to content

Commit f7a2d77

Browse files
authored
Add dAuth job secret endpoints (#458)
* feat: add dauth job secret endpoints * fix: require job_secrets bundle shape * fix: simplification * fix: protect dAuth secret requests from replay * fix * feat: sync dAuth secrets through registry peers * fix: refresh dAuth registry authorization * refactor: simplify dAuth registry refresh * chore: inc ver
1 parent 313715c commit f7a2d77

7 files changed

Lines changed: 1001 additions & 16 deletions

File tree

AGENTS.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,3 +695,30 @@ Entry format:
695695
- Details: `ThHfModelBase` keeps Transformers/PT as the default GPU and fallback path, but CPU-only `HF_RUNTIME=auto` now loads `artifact_manifest.json`, selects a declared ONNX Runtime artifact, downloads only safe allow-patterns, loads schema and contract decoder from HF artifacts, and exposes the decoded artifact contract through the existing text-classifier flow. Business API response shaping now passes through generic model/runtime metadata emitted by serving.
696696
- Verification: `python3 -m unittest extensions.serving.test_th_hf_model_base extensions.serving.test_th_text_classifier extensions.serving.test_th_privacy_filter extensions.business.edge_inference_api.test_text_classifier_inference_api extensions.business.edge_inference_api.test_privacy_filter_inference_api`; `python3 -m py_compile extensions/serving/default_inference/nlp/th_hf_model_base.py extensions/business/edge_inference_api/text_classifier_inference_api.py`; required serving gate `python3 -m unittest extensions.serving.model_testing.test_llm_servings` currently fails at import with `ImportError: cannot import name 'Logger' from 'naeural_core'`.
697697
- Links: `extensions/serving/default_inference/nlp/th_hf_model_base.py`, `extensions/business/edge_inference_api/text_classifier_inference_api.py`, `extensions/serving/test_th_hf_model_base.py`
698+
699+
- ID: `ML-20260723-001`
700+
- Timestamp: `2026-07-23T13:45:20Z`
701+
- Type: `change`
702+
- Summary: dAuth job-secret requests now require signed 120-second timestamp nonces, and GET responses encrypt secret bundles to the authorized runner.
703+
- Criticality: Security protocol change preventing indefinite signed-request/response replay and removing plaintext job secrets from HTTP responses.
704+
- Details: `/add_secrets` and `/get_secrets` validate signed hex-millisecond timestamp nonces and echo them in successful signed responses. `/get_secrets` encrypts the serialized bundle to the signed requester address; clients must verify the response signer and echoed nonce before decrypting.
705+
- Verification: `python -m unittest discover -s extensions/business/dauth -p 'test_*.py'`; cross-repo SDK dAuth client tests.
706+
- Links: `extensions/business/dauth/dauth_mixin.py`, `extensions/business/dauth/dauth_manager.py`
707+
708+
- ID: `ML-20260731-001`
709+
- Timestamp: `2026-07-31T16:48:11Z`
710+
- Type: `change`
711+
- Summary: dAuth job-secret ChainStore writes and minute syncs now target only startup-cached dAuth registry peers.
712+
- Criticality: Secret-replication boundary and recovery behavior across every dAuth server.
713+
- Details: The dAuth manager reads registry ETH addresses once at startup, keeps local service eligibility fixed until restart, and refreshes only ETH-to-internal mappings from local NetMon state. `DAUTH_JOB_SECRETS` writes and 60-second hsync calls disable default/configured ChainStore peers. Known deferred risks: generic ChainStore does not authorize inbound operations by hash namespace, and first-response hsync has no freshness arbitration; production hardening requires an inbound ACL or dedicated authenticated replication protocol plus version-aware merges.
714+
- Verification: `python3 -m unittest discover -s extensions/business/dauth -p 'test_*.py'`; `python3 -m py_compile extensions/business/dauth/dauth_registry.py extensions/business/dauth/dauth_manager.py extensions/business/dauth/dauth_mixin.py extensions/business/dauth/test_dauth_registry_gating.py extensions/business/dauth/test_dauth_secret_routing.py`; `git diff --check`
715+
- Links: `extensions/business/dauth/dauth_registry.py`, `extensions/business/dauth/dauth_manager.py`, `extensions/business/dauth/dauth_mixin.py`
716+
717+
- ID: `ML-20260803-001`
718+
- Timestamp: `2026-08-03T16:09:26Z`
719+
- Type: `change`
720+
- Summary: dAuth server eligibility and secret-replication peers now refresh from the on-chain registry every hour; secret hsync runs every 10 minutes.
721+
- Criticality: Authorization revocation and secret-replication routing across every dAuth server.
722+
- Details: Lifecycle pause/resume predicates perform the rate-limited registry refresh without adding RPC calls to endpoint request paths. Successful reads remain cached for one hour; failed reads clear cached peers, fail closed, and retry after one minute. Registry reads are synchronous and rely on the SDK Web3 provider to return or time out. A removed local node causes the web app to pause and become unready; readiness returns only after a resumed Uvicorn process reports startup. Remaining dAuth nodes replace their cached peer set on their next hourly refresh. The inbound namespace authorization and version-aware hsync limitations from `ML-20260731-001` remain open.
723+
- Verification: `python3 -m unittest discover -s extensions/business/dauth -p 'test_*.py'`; `python3 -m py_compile extensions/business/dauth/dauth_registry.py extensions/business/dauth/dauth_manager.py extensions/business/dauth/dauth_mixin.py extensions/business/dauth/test_dauth_registry_gating.py extensions/business/dauth/test_dauth_secret_routing.py`; `git diff --check`
724+
- Links: `extensions/business/dauth/dauth_manager.py`, `extensions/business/dauth/test_dauth_registry_gating.py`

extensions/business/dauth/dauth_manager.py

Lines changed: 150 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@
2323
from extensions.business.mixins.node_tags_mixin import _NodeTagsMixin
2424
from naeural_core.business.default.web_app.supervisor_fast_api_web_app import SupervisorFastApiWebApp as BasePlugin
2525
from extensions.business.mixins.request_tracking_mixin import _RequestTrackingMixin
26-
from extensions.business.dauth.dauth_mixin import _DauthMixin
26+
from extensions.business.dauth.dauth_mixin import (
27+
DAUTH_JOB_SECRETS_CSTORE_HKEY,
28+
_DauthMixin,
29+
)
30+
from extensions.business.dauth.dauth_registry import (
31+
dauth_registry_write_kwargs,
32+
load_dauth_registry_snapshot,
33+
)
2734

2835
__VER__ = '0.3.0'
2936

@@ -42,6 +49,10 @@
4249
'REQUESTS_MAX_RECORDS': 2,
4350
'REQUESTS_LOG_INTERVAL': 5 * 60,
4451

52+
'DAUTH_JOB_SECRETS_HSYNC_INTERVAL': 10 * 60,
53+
'DAUTH_REGISTRY_REFRESH_INTERVAL': 60 * 60,
54+
'DAUTH_REGISTRY_REFRESH_RETRY_INTERVAL': 60,
55+
4556
'SUPRESS_LOGS_AFTER_INTERVAL' : 300,
4657

4758
# required ENV keys are defined in plugin template and should be added here
@@ -105,6 +116,11 @@ def __init__(self, **kwargs):
105116
super(DauthManagerPlugin, self).__init__(**kwargs)
106117
self._dauth_server_enabled = None
107118
self._dauth_server_enabled_message = None
119+
self._dauth_registry_eth_oracles = None
120+
self._dauth_registry_internal_peers = None
121+
self._last_dauth_registry_refresh = None
122+
self._dauth_registry_refresh_failed = False
123+
self._last_dauth_job_secrets_hsync = None
108124
self._dauth_web_app_initialized = False
109125
self._dauth_pause_teardown_succeeded = True
110126
return
@@ -117,6 +133,8 @@ def on_init(self):
117133
self._dauth_web_app_initialized = True
118134
if not self._is_dauth_server_enabled():
119135
self.on_pause()
136+
else:
137+
self._maybe_hsync_dauth_job_secrets()
120138
# endif
121139
my_address = self.bc.address
122140
my_eth_address = self.bc.eth_address
@@ -132,20 +150,52 @@ def _check_dauth_server_enabled_on_start(self):
132150
return self._dauth_server_enabled
133151
# endif
134152

153+
return self._refresh_dauth_registry(force=True)
154+
155+
def _refresh_dauth_registry(self, force=False):
156+
now = self.time()
157+
last_refresh = getattr(self, "_last_dauth_registry_refresh", None)
158+
refresh_interval = (
159+
self.cfg_dauth_registry_refresh_retry_interval
160+
if getattr(self, "_dauth_registry_refresh_failed", False)
161+
else self.cfg_dauth_registry_refresh_interval
162+
)
163+
if (
164+
not force
165+
and last_refresh is not None
166+
and now - last_refresh < refresh_interval
167+
):
168+
return self._is_dauth_server_enabled()
169+
# endif
170+
171+
self._last_dauth_registry_refresh = now
172+
previous_enabled = getattr(self, "_dauth_server_enabled", None)
173+
previous_eth_oracles = getattr(self, "_dauth_registry_eth_oracles", None)
174+
135175
error = None
136176
try:
137-
enabled = self.bc.is_dauth_oracle() is True
177+
peers, eth_oracles = load_dauth_registry_snapshot(self)
178+
enabled = self.bc.eth_address.lower() in [
179+
address.lower() for address in eth_oracles
180+
]
138181
except Exception as e:
139182
enabled = False
140183
error = str(e)
141184
# end try
142185

143186
message = None if enabled else error or "current node is not registered as a dAuth oracle"
187+
self._dauth_registry_eth_oracles = eth_oracles if enabled else None
188+
self._dauth_registry_internal_peers = peers if enabled else None
189+
self._dauth_registry_refresh_failed = error is not None
144190
self._dauth_server_enabled = enabled
145191
self._dauth_server_enabled_message = message
146-
if enabled:
147-
self.P(f"{self.__class__.__name__} dAuth registry gate is enabled")
148-
else:
192+
registry_changed = previous_eth_oracles != self._dauth_registry_eth_oracles
193+
if enabled and (previous_enabled is not True or registry_changed):
194+
self.P(
195+
f"{self.__class__.__name__} dAuth registry gate is enabled "
196+
f"with {len(eth_oracles)} registered oracle(s)"
197+
)
198+
elif not enabled and (previous_enabled is not False or error is not None):
149199
self.P(
150200
f"{self.__class__.__name__} dAuth registry gate is disabled. "
151201
f"(cause: {message})",
@@ -159,16 +209,19 @@ def _is_dauth_server_enabled(self):
159209
return getattr(self, "_dauth_server_enabled", None) is True
160210

161211
def should_pause(self):
212+
self._refresh_dauth_registry()
162213
return not self._is_dauth_server_enabled()
163214

164215
def should_resume(self):
216+
self._refresh_dauth_registry()
165217
return self._is_dauth_server_enabled()
166218

167219
def on_pause(self):
168220
if not getattr(self, "_dauth_web_app_initialized", False):
169221
return
170222
# endif
171223

224+
self.set_plugin_ready(False)
172225
self._dauth_pause_teardown_succeeded = False
173226
self._stop_request_monitor.set()
174227
if self._request_monitor_thread is not None:
@@ -226,6 +279,13 @@ def on_resume(self):
226279
self._stop_request_monitor.clear()
227280
self._start_request_monitor_thread()
228281
return
282+
283+
def on_log_handler(self, text, key=None):
284+
super(DauthManagerPlugin, self).on_log_handler(text, key=key)
285+
if self._is_dauth_server_enabled() and "Uvicorn running on " in text:
286+
self.set_plugin_ready(True)
287+
# endif
288+
return
229289

230290

231291
def on_request(self, request):
@@ -237,11 +297,34 @@ def on_response(self, method, response):
237297
return
238298

239299
def process(self):
300+
self._maybe_hsync_dauth_job_secrets()
240301
# TODO: this will be re-enabled in the future.
241302
if False:
242303
self._maybe_log_and_save_tracked_requests()
243304
return
244305

306+
def _maybe_hsync_dauth_job_secrets(self):
307+
if not self._is_dauth_server_enabled():
308+
return None
309+
310+
now = self.time()
311+
last_sync = getattr(self, "_last_dauth_job_secrets_hsync", None)
312+
if (
313+
last_sync is not None
314+
and now - last_sync < self.cfg_dauth_job_secrets_hsync_interval
315+
):
316+
return None
317+
318+
self._last_dauth_job_secrets_hsync = now
319+
try:
320+
return self.chainstore_hsync(
321+
hkey=DAUTH_JOB_SECRETS_CSTORE_HKEY,
322+
**dauth_registry_write_kwargs(self),
323+
)
324+
except Exception as exc:
325+
self.P(f"Could not sync dAuth job secrets: {exc}", color="y")
326+
return None
327+
245328
def __get_current_epoch(self):
246329
"""
247330
Get the current epoch of the node.
@@ -343,3 +426,65 @@ def get_auth_data(self, body: dict):
343426
**data
344427
})
345428
return response
429+
430+
@BasePlugin.endpoint(method="post")
431+
# /add_secrets
432+
def add_secrets(self, body: dict):
433+
"""
434+
Store a full job secret bundle from a protocol oracle.
435+
436+
The signed request must include a hex-millisecond timestamp nonce no older
437+
than 120 seconds.
438+
"""
439+
request_nonce = body.get("nonce") if isinstance(body, dict) else None
440+
if not self._is_dauth_server_enabled():
441+
response = self.__get_response({
442+
'error': 'dAuth server is not registered as a dAuth oracle',
443+
'nonce': request_nonce,
444+
})
445+
return response
446+
447+
try:
448+
data = self.process_dauth_add_secrets_request(body)
449+
except Exception as e:
450+
self.P("Error processing add_secrets request: {}".format(e), color='r')
451+
data = {
452+
'error' : str(e)
453+
}
454+
455+
response = self.__get_response({
456+
'nonce': request_nonce,
457+
**data
458+
})
459+
return response
460+
461+
@BasePlugin.endpoint(method="post")
462+
# /get_secrets
463+
def get_secrets(self, body: dict):
464+
"""
465+
Return an encrypted job secret bundle to a current R1FS job runner.
466+
467+
The signed request must include a hex-millisecond timestamp nonce no older
468+
than 120 seconds. The signed response echoes that nonce.
469+
"""
470+
request_nonce = body.get("nonce") if isinstance(body, dict) else None
471+
if not self._is_dauth_server_enabled():
472+
response = self.__get_response({
473+
'error': 'dAuth server is not registered as a dAuth oracle',
474+
'nonce': request_nonce,
475+
})
476+
return response
477+
478+
try:
479+
data = self.process_dauth_get_secret_request(body)
480+
except Exception as e:
481+
self.P("Error processing get_secrets request: {}".format(e), color='r')
482+
data = {
483+
'error' : str(e)
484+
}
485+
486+
response = self.__get_response({
487+
'nonce': request_nonce,
488+
**data
489+
})
490+
return response

0 commit comments

Comments
 (0)