Task: SDK State/Serialization Layer + Daemon Preferences & Capability Detection (Issues #20 partial / #21 partial)
ID: task_220726_7
Assigned to: Santiago Rodriguez Cetran (@Santiagocetran)
Created: 2026-07-22
Status: Open
Repo: https://github.com/InfiniteZeroFoundation/DevNet
Base branches: feat/din-sdk (PR #31, draft) for Part A · feat/din-daemon (PR #32, draft, stacked on feat/din-sdk) for Part B
Roadmap ref: P4-1.2 (partial), P4-2.1, P4-2.2 (partial — hardware/local only, no RPC-session dependency)
Dates: Jul 22–28, 2026 (5 working days; deadline Tuesday, July 28)
This continues the work already visible in draft PRs #31 (feat/din-sdk) and #32 (feat/din-daemon) — both opened early per the "keep feature-branch work visible" convention, neither merge-ready yet. Reviewed directly against the checked-out branches (not just the PR descriptions, which lag the actual commits):
dincli/sdk/currently hascid,errors,config,log,web3,ipfs,contracts,manifest,runtime— 9 of the 16 modules inDeveloper/design/sdk-interface-proposal.md§8's proposed layout.errors.pyalready implements the fullDinErrortaxonomy and allowlist-basedsanitize_detailsfrom the proposal's §4/§4a — further along than PR #31's own body claims.dincli/dind/implements the full P4-1.1 scope: lifecycle (start/stop/status), SQLite job queue, graceful SIGTERM,/health, structured JSON logging, systemd/launchd examples, container handoff. Confirmed by readingdincli/dind/*.pydirectly, not just the PR body.
What's still missing from the SDK, per the proposal's §8 layout: session.py (DinSession), wallet.py, state.py, serialize.py, tx.py, worker.py, and the operations/ role-command layer. That's a lot — too much for one week, and the biggest pieces (wallet.py/session.py) touch private-key/keystore handling directly.
Deliberate scope call for this task: split off the two lowest-risk, most self-contained pieces — state.py + serialize.py on the SDK side, and preferences + hardware capability detection on the daemon side (P4-2.1/P4-2.2, both of which are local-only and don't need a working DinSession). The wallet/session/signing keystone (§2 of the proposal) is deliberately not in this task — see Scope boundaries below.
If anything here conflicts with what you find on feat/din-sdk/feat/din-daemon, flag it — this spec was written against those branches as of 2026-07-22 (PR #31 @ ba5a54d, PR #32 @ ecf7aa3).
Scope boundaries — read before starting:
- No wallet/keystore/signing work.
dincli/sdk/wallet.py,session.py(DinSession), and theSignerProviderprotocol (proposal §2) are explicitly deferred to a dedicated future task. Private-key/decryption logic deserves its own focused review pass, not a shared slot with unrelated daemon work. Don't start it even if this week's scope finishes early — flag it as the next task instead. tx.py'ssend()keystone (proposal §5b) is out of scope — it takes aDinSession, which doesn't exist yet.- The
operations/role-command refactor is out of scope. CLI commands can't be refactored onto SDK operations that sign transactions untilwallet.py/session.pyland. - P4-2.2's RPC/IPFS "network connectivity" check stays a lightweight reachability probe (socket connect / DNS resolve with a short timeout) — not a real RPC call through a session. Don't reach for
dincli.sdk.web3.get_w3here in a way that requires a live signer; a bare connectivity check doesn't need one, but don't grow it into more than that. - No changes to existing CLI command behavior or output.
state.py/serialize.pyare additive; existing call sites keep working exactly as today (see §1 below — same shim pattern as the two prior extraction waves).
Extract from dincli/cli/utils.py:
GIstateToDes(line 385)GIstateToStr(line 393)GIstatestrToIndex(line 405)
Move them verbatim into dincli/sdk/state.py (pure functions, no console/typer dependency already — confirm before moving, don't assume). Reduce cli/utils.py to re-export these from the new module, same pattern already used for services/ipfs.py and services/runtime.py in the prior wave (85ceae7) — a docstring noting the move plus from dincli.sdk.state import ... # noqa: F401.
Add the validate_* predicates the proposal's §8 layout calls for alongside the converters — start with whatever GI-state validation logic today's callers currently inline (grep GIstate == / GIstate in across cli/modelownerd/) and lift the repeated checks into named predicates here (e.g. validate_gi_open(gi_state) -> None, raising dincli.sdk.errors.ValidationError instead of each call site's own ad hoc check). Don't invent new validation rules — only extract what's already enforced somewhere in the CLI today.
Per proposal §3. This is genuinely new code (no existing --json call sites to preserve behavior against), so the correctness bar is the serialization rules table itself:
# dincli/sdk/serialize.py
def to_envelope(data: "DataclassInstance | None", *, error: DinError | None = None,
network: str | None = None, chain_id: int | None = None,
correlation_id: str | None = None) -> dict:
"""Wrap a dataclass (or an error) in the {status, data, error, meta} envelope (proposal §3)."""Encoder rules to implement and test explicitly (proposal §3's "Serialization rules"):
- Fields tagged
metadata={"json": "uint256_string"}→ decimal string, not a JSON number (precision safety for JS consumers). - Fields tagged
metadata={"json": "omit"}(e.g.TxReceiptInfo._raw, when that type eventually exists) → dropped entirely from the envelope, never partially serialized. - Small integers (GI number, indices, plain enum-backed ints) stay JSON numbers — don't stringify indiscriminately.
bytes/HexBytes→0x-prefixed hex string.- Addresses → checksummed string (reuse
Web3.to_checksum_addressor equivalent, don't hand-roll). - Enums → name string (not the int value) in
data; the proposal allows including the int indetailson the error path, not here. Decimal→ string.
There's nothing in the SDK yet with a uint256_string-tagged field to serialize against — write the encoder against a small test-only dataclass fixture that exercises every rule above, plus one real one: wrap dincli.sdk.state's GI-state enum in a trivial dataclass and confirm it serializes to its name string. Don't force an envelope onto manifest.py/contracts.py outputs this task — that's operations/'s job later, out of scope here (boundary #3).
tests/test_sdk_state.py(new): round-tripGIstateToDes/GIstateToStr/GIstatestrToIndexunchanged from today's behavior (snapshot the current outputs before moving code, diff after); each newvalidate_*predicate raisesValidationErroron the case it's supposed to catch and passes silently otherwise.tests/test_sdk_serialize.py(new): one test per encoder rule above (uint256 stringification, omit, small-int passthrough, bytes→hex, checksummed address, enum→name,Decimal→string); one round-trip test throughto_envelope()on both the success and error paths (error path must use a realDinErrorsubclass and asserterror.code/error.detailsland correctly — reusesanitize_detailsbehavior already tested intest_sdk_boundary.py, don't re-test it here).- Extend
tests/test_sdk_boundary.py's fresh-subprocess import check to includestateandserializein the module list asserted clean oftyper/rich/dincli.cli.*.
Both build directly on dincli/dind/config.py's existing resolve_* precedence pattern (--flag > env > config.json > default, lines 19–64) and dincli/dind/main.py's existing start/stop/status command structure (lines 46/123/161) — mirror those conventions, don't invent a new config-loading style.
Minimal schema per the roadmap wording — don't over-build:
@dataclass
class Preferences:
domain: str | None = None # e.g. "vision", "nlp" — free-form for now, no fixed taxonomy
risk_tolerance: str = "moderate" # "conservative" | "moderate" | "aggressive"
min_expected_reward: int | None = None # wei; None = no floor
privacy_constraints: list[str] = field(default_factory=list) # free-form tagsStorage: a preferences.json file under the resolved state dir (same dir family as the SQLite job store — see dincli/dind/paths.py for the existing path helpers, reuse them rather than hardcoding a new path). Load/save functions following the same shape as dincli/dind/state.py's StateStore — doesn't need to be a class if a pair of load_preferences()/save_preferences() functions is simpler; match whichever style state.py already established.
New commands in dincli/dind/main.py:
dind preferences show— prints current preferences (JSON, since this is daemon-facing infra — reuse the JSON-logging conventions already indincli/dind/logging.pyfor consistent formatting, don't add arich-table dependency todindwhen nothing else in the package uses one).dind preferences set --domain ... --risk-tolerance ... --min-reward ... --privacy ...— partial updates only touch the flags passed; unset flags leave the existing stored value untouched.
Detect, stdlib-first (avoid adding a new dependency unless there's genuinely no stdlib path — flag it in the PR if you do):
- CPU count (
os.cpu_count()— already used inhealth.py:83, reuse it) and, if easily available without a new dependency, CPU speed. - Total/free RAM — no stdlib cross-platform API exists for this; either add
psutil(flag the new dependency explicitly in the PR, it's the standard choice) or document that RAM detection is deferred pending that decision. Don't silently skip it without a note. - Disk free under the resolved state dir — already computed in
health.py:66-73(shutil.disk_usage); factor it into a shared helper bothcapabilities.pyandhealth.pycall, rather than duplicating the try/except. - GPU availability — best-effort only. A simple presence check (e.g.
nvidia-smionPATH, or checking for/dev/nvidia*) is enough; don't pull in a GPU-detection library for this. Absence is a valid, common result — don't treat it as an error. - Network connectivity to RPC and IPFS — per scope boundary #5, this is a bare reachability probe (
socket.create_connectionwith a short timeout against the configured RPC/IPFS host:port, or a DNS resolve if that's simpler), not a live RPC call. Read the RPC/IPFS endpoints the same waydincli.sdk.configalready resolves them for the CLI — don't invent a second config path for this.
@dataclass
class CapabilitySummary:
cpu_count: int
cpu_speed_mhz: int | None
ram_total_bytes: int | None
ram_free_bytes: int | None
disk_free_bytes: int | None
disk_total_bytes: int | None
gpu_available: bool
rpc_reachable: bool | None # None = check skipped/not configured
ipfs_reachable: bool | None
def detect_capabilities() -> CapabilitySummary: ...
def score_capabilities(summary: CapabilitySummary) -> int: ...
# simple weighted score — document the weights inline, they're a starting point, not final
def compatible_with(summary: CapabilitySummary, requirements: dict) -> bool: ...
# placeholder shape for later task-matching (P4-3.x); requirements schema isn't defined yet,
# so keep this minimal — a dict of {"min_ram_bytes": ..., "min_disk_bytes": ..., "requires_gpu": ...}
# is enough to prove the filter works, don't over-design against a spec that doesn't exist yetNew command: dind capabilities — prints the CapabilitySummary as JSON.
Extend /health's existing resources block (dincli/dind/health.py:82-86, inside _build_payload which starts at line 49) to include the fuller summary from capabilities.py instead of just cpu_count/disk_free_bytes/disk_total_bytes — call detect_capabilities() (or cache it; detecting on every health poll may be wasteful, your call, but document the tradeoff if you cache) rather than duplicating the CPU/disk logic a second time in health.py.
tests/test_dind_preferences.py(new): round-trip save/load; partialsetonly updates the touched fields;showon a fresh state dir returns defaults without erroring.tests/test_dind_capabilities.py(new):detect_capabilities()runs without error on the test host (mock/skip GPU and network checks where the CI environment can't guarantee a result — don't assert a specific GPU/network outcome, only that the function returns a well-formedCapabilitySummary);score_capabilities()andcompatible_with()are pure and unit-testable directly (no host dependency); extendtests/test_dind_boundary.py's import-boundary check ifcapabilities.py/preferences.pyneed to be asserteddincli.cli-free too (confirm whether that boundary test currently covers all ofdincli.dindor just specific modules before assuming).- Extend
tests/test_dind_health.py:/health'sresourcesblock includes the new fields once wired.
Part A — SDK (#20 partial)
-
dincli/sdk/state.pyextracted fromcli/utils.py, old call sites re-exported as a shim (same pattern as the prior wave) -
dincli/sdk/serialize.py(to_envelope()+ encoder) implementing every rule in proposal §3 - New SDK tests; import-boundary test extended to cover both new modules
- Existing test suite still green — no CLI behavior change
Part B — Daemon (#21 partial)
-
dind preferences show/setbacked bydincli/dind/preferences.py -
dind capabilitiesbacked bydincli/dind/capabilities.py;/health'sresourcesblock extended to match - New daemon tests; existing
dindtest suite still green -
forge-equivalent for Python:pytestclean across both branches - Both PRs (#31, #32) updated with the new commits; note in each PR body which slice this task covers
- Discussion posted in the Tasks category, referencing this task file and flagging wallet/session/tx as the explicitly-deferred next slice
- Release valve if the 5-day budget runs tight: cut Part B's GPU detection and the
compatible_with()placeholder first — they're the least load-bearing pieces (nothing callscompatible_withyet; P4-3.x task matching is months out). Do not cutstate.py/serialize.py— small, low-risk, and every lateroperations/module depends on the envelope shape being right. - The next task after this one is the wallet/session/tx keystone (proposal §2, §5b) — flag it for Umer's review given the private-key/signing sensitivity, same as Robbert's treasury/fee-router work got flagged for cryptoeconomic sensitivity. Don't start it early under this task's scope even if time permits (boundary #1).
feat/din-daemon(PR #32) is stacked onfeat/din-sdk(PR #31) — push Part A first and consider rebasing Part B's branch onto it partway through the week so the two PRs don't drift further apart, though nothing in Part B functionally depends on Part A's output this round.- Re-pull and re-run the full suite on both branches right before opening/updating either PR, per the existing convention —
develop's tip may have moved.