This issue is about how custom service functions should access manifest data at runtime.
Today dincli dynamically loads service functions from task-specific files such as:
cache_model_0/services/client.pycache_model_0/services/auditor.pycache_model_0/services/aggregator.pycache_model_0/services/modelowner.py
Those services are meant to be customizable and manifest-driven. However, in practice, manifest access is still controlled from the CLI layer. That creates a gap between the documented flexibility of services and what service code can actually do cleanly.
The current architecture allows dincli to load custom functions through ctx.obj.load_custom_fn(), but the loaded service code does not receive a standard runtime context for reading manifest values.
This becomes a problem when a service needs to read custom manifest fields dynamically, for example:
- training hyperparameters
- model-specific paths
- custom feature flags
- aggregation settings
- auditor thresholds
- differential privacy parameters
- task-specific metadata defined by a model owner
There is already a helper in dincli.cli.utils:
get_manifest_key(network, key, model_id=None, task_coordinator_address=None)
And the documentation in Documentation/public/manifest.md says custom manifest fields can be accessed in services through get_manifest_key.
But there is a practical issue:
get_manifest_keyrequires runtime identifiers such asnetwork,model_id, ortask_coordinator_address- those values are usually known in the CLI command flow, not inside the service module itself
- service functions loaded by
load_custom_fn()do not receive a standard context object that carries those values
As a result, service authors either have to:
- hardcode behavior in the service function name or file layout
- duplicate configuration outside the manifest
- rely on implicit globals or directory assumptions
- add ad hoc parameters differently for each service entrypoint
That is not a clean production design.
Relevant code paths today:
dincli/cli/context.pyload_custom_fn()dynamically imports a function from a service file
dincli/cli/utils.pyget_manifest_key()reads manifest data from cache or task directories
Documentation/public/manifest.md- states that custom manifest fields can be accessed in services
The current loading mechanism returns a callable, but does not inject:
networkmodel_idtask_coordinator_address- manifest path
- parsed manifest contents
- a service runtime helper
So the service is loaded successfully, but it still lacks a standard way to discover its manifest context.
This limitation reduces the main value of custom services.
Without safe manifest access inside services:
- service code becomes less reusable across tasks
- simple customizations turn into CLI changes
- manifest-driven behavior becomes partially hardcoded
- documentation promises more flexibility than the runtime actually provides
- debugging becomes harder because service behavior depends on hidden calling conventions
It also creates a boundary problem:
- the CLI layer knows the runtime identity of the task
- the service layer contains the task-specific logic
- but there is no formal contract between them for configuration access
That missing contract is the root issue.
Any fix should support the following:
- Services can read manifest values without hardcoding model-specific assumptions.
- The runtime contract is explicit and consistent across client, auditor, aggregator, and model owner services.
- The service does not need to reconstruct task identity from fragile directory conventions.
- The approach works for both model-based manifests (
model_id) and task-based manifests (task_coordinator_address). - The manifest access path is testable and easy to mock.
- The design does not tightly couple custom services to internal CLI implementation details.
Example direction:
from dincli.cli.utils import get_manifest_keyThen service code would call it directly when needed.
- minimal code change
- easy to adopt quickly
- keeps manifest lookup logic in one existing helper
- the service still needs
networkandmodel_idortask_coordinator_address - there is no standard source for those values inside the service
- this couples service code to
dincli.cli.utils, which is a CLI-oriented module - it makes service code depend on a larger utility layer than necessary
- it does not define a proper runtime interface
This is acceptable as a temporary workaround, but not as the long-term design.
Example direction:
fn(..., network=effective_network, model_id=model_id)- explicit
- easy to understand
- avoids hidden globals
- every service function signature must change
- different commands may pass slightly different context values
- some flows use
task_coordinator_addressinstead ofmodel_id - entrypoints become inconsistent unless standardized carefully
- future additions such as manifest path, role, cache directory, or logger will keep expanding signatures
This is better than Option 1, but it does not scale well.
Example direction:
fn(..., manifest=manifest_data)- simple for service authors
- avoids repeated file reads
- decouples service logic from manifest lookup implementation
- the function loses access to the manifest identity and source metadata unless more fields are also passed
- stale data handling becomes the CLI's responsibility
- every caller must remember to pass the manifest
- service code may still need helper methods for nested lookups, validation, and defaults
This is a reasonable intermediate design, but still incomplete for a production runtime contract.
Example direction:
class ServiceRuntimeContext:
network: str
model_id: int | None
task_coordinator_address: str | None
manifest_path: Path
manifest: dict
def get_manifest_key(self, key: str, default=None):
...Then loaded service functions would receive:
fn(..., runtime=service_runtime)- explicit and extensible
- one standard runtime contract for all services
- manifest access is simple and local to the service
- future runtime features can be added without breaking every function signature
- easier unit testing because the runtime object can be mocked
- better separation between CLI orchestration and service logic
- requires coordinated refactor of service entrypoints
- needs a compatibility strategy for existing services
This is the strongest architectural direction.
The production-grade solution is to introduce a formal service execution context and treat manifest access as part of that interface, not as an incidental helper import.
Create a small runtime object for dynamically loaded services, for example:
class ServiceRuntimeContext:
def __init__(
self,
network,
model_id=None,
task_coordinator_address=None,
manifest_path=None,
manifest=None,
role=None,
):
self.network = network
self.model_id = model_id
self.task_coordinator_address = task_coordinator_address
self.manifest_path = manifest_path
self.manifest = manifest or {}
self.role = role
def get_manifest_key(self, key, default=None):
return self.manifest.get(key, default)This object should be owned by the service/runtime layer, not by ad hoc command code.
The CLI command already knows the effective runtime identity:
- network
- model id
- task coordinator address
It should resolve the correct manifest once, then pass that resolved manifest into the service runtime context.
That gives:
- one authoritative manifest snapshot per invocation
- fewer repeated file reads
- cleaner error handling
- a single place for freshness checks and cache policy
Instead of only loading a bare callable, define a standard calling convention for manifest-aware services.
For example:
fn(..., runtime=runtime)This preserves flexibility while avoiding a growing list of positional and keyword parameters.
To avoid breaking existing custom services immediately:
- detect whether the loaded function accepts
runtime - pass it when supported
- keep legacy invocation behavior for older services during a transition period
That makes adoption incremental.
If service code needs shared manifest helpers, they should live in a runtime-safe module such as:
dincli.services.runtimedincli.services.manifest
This avoids making custom services depend on CLI-oriented utility code.
A direct import solves only the lookup function location. It does not solve runtime identity, interface stability, or testability.
A runtime context solves:
- where the manifest comes from
- how it is accessed
- how service code receives task identity
- how future execution metadata is added
That is the difference between a convenience patch and a maintainable production design.
If this is implemented for real production use, the design should also address:
- manifest schema validation before service execution
- clear errors when required keys are missing
- immutable or read-only manifest access during execution
- versioning of the service runtime contract
- structured logging for service-level manifest access failures
- test fixtures for runtime context injection
These are important because once custom services become the main extension mechanism, the runtime contract becomes part of the platform API.
- Decide whether the short-term goal is a workaround or a proper runtime contract.
- If short-term, allow
runtimeinjection while keeping old service signatures working. - Add a small
ServiceRuntimeContextimplementation and use it in one service path first. - Update documentation so it reflects the actual supported pattern for manifest access.
- Migrate sample services in
cache_model_0/services/to the new interface.
Yes, services should be able to access manifest data in a flexible way.
But the correct solution is not just "import get_manifest_key() from inside the service". The real requirement is a formal runtime contract that gives services safe access to manifest data and task identity.
For production-quality architecture, the recommended solution is:
- resolve manifest context in the CLI layer
- inject a standard runtime context into service functions
- expose manifest access through that runtime object
That keeps services flexible, testable, and aligned with the manifest-driven design the platform is already aiming for.