Skip to content

Latest commit

 

History

History
170 lines (124 loc) · 15.7 KB

task_220726_7.md

File metadata and controls

170 lines (124 loc) · 15.7 KB

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)


Context

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 has cid, errors, config, log, web3, ipfs, contracts, manifest, runtime — 9 of the 16 modules in Developer/design/sdk-interface-proposal.md §8's proposed layout. errors.py already implements the full DinError taxonomy and allowlist-based sanitize_details from 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 reading dincli/dind/*.py directly, 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:

  1. No wallet/keystore/signing work. dincli/sdk/wallet.py, session.py (DinSession), and the SignerProvider protocol (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.
  2. tx.py's send() keystone (proposal §5b) is out of scope — it takes a DinSession, which doesn't exist yet.
  3. The operations/ role-command refactor is out of scope. CLI commands can't be refactored onto SDK operations that sign transactions until wallet.py/session.py land.
  4. 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_w3 here 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.
  5. No changes to existing CLI command behavior or output. state.py/serialize.py are additive; existing call sites keep working exactly as today (see §1 below — same shim pattern as the two prior extraction waves).

Part A — SDK: state.py + serialize.py (Issue #20, P4-1.2 continued)

1. dincli/sdk/state.py — GI-state enums/converters

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.

2. dincli/sdk/serialize.py — to_envelope() + shared encoder

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_address or equivalent, don't hand-roll).
  • Enums → name string (not the int value) in data; the proposal allows including the int in details on 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).

3. Tests (Part A)

  • tests/test_sdk_state.py (new): round-trip GIstateToDes/GIstateToStr/GIstatestrToIndex unchanged from today's behavior (snapshot the current outputs before moving code, diff after); each new validate_* predicate raises ValidationError on 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 through to_envelope() on both the success and error paths (error path must use a real DinError subclass and assert error.code/error.details land correctly — reuse sanitize_details behavior already tested in test_sdk_boundary.py, don't re-test it here).
  • Extend tests/test_sdk_boundary.py's fresh-subprocess import check to include state and serialize in the module list asserted clean of typer/rich/dincli.cli.*.

Part B — Daemon: Preferences (P4-2.1) + Hardware Capability Detection (P4-2.2, local-only slice)

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.

4. dincli/dind/preferences.py — local preference schema (P4-2.1)

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 tags

Storage: 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 in dincli/dind/logging.py for consistent formatting, don't add a rich-table dependency to dind when 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.

5. dincli/dind/capabilities.py — hardware/local capability detection (P4-2.2, hardware-only)

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 in health.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 both capabilities.py and health.py call, rather than duplicating the try/except.
  • GPU availability — best-effort only. A simple presence check (e.g. nvidia-smi on PATH, 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_connection with 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 way dincli.sdk.config already 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 yet

New 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.

6. Tests (Part B)

  • tests/test_dind_preferences.py (new): round-trip save/load; partial set only updates the touched fields; show on 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-formed CapabilitySummary); score_capabilities() and compatible_with() are pure and unit-testable directly (no host dependency); extend tests/test_dind_boundary.py's import-boundary check if capabilities.py/preferences.py need to be asserted dincli.cli-free too (confirm whether that boundary test currently covers all of dincli.dind or just specific modules before assuming).
  • Extend tests/test_dind_health.py: /health's resources block includes the new fields once wired.

Deliverables

Part A — SDK (#20 partial)

  • dincli/sdk/state.py extracted from cli/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/set backed by dincli/dind/preferences.py
  • dind capabilities backed by dincli/dind/capabilities.py; /health's resources block extended to match
  • New daemon tests; existing dind test suite still green
  • forge-equivalent for Python: pytest clean 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

Notes

  • 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 calls compatible_with yet; P4-3.x task matching is months out). Do not cut state.py/serialize.py — small, low-risk, and every later operations/ 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 on feat/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.