HiNet.

Documentation  /  Onboarding & agent harness

HiNet Onboarding Funnel & Agent Harness — Specification

Doc status: Draft v0.1 · Scope: the in‑app first‑run funnel (PART A) + HiNet's own agent harness with dev‑tool integration (PART B) · Grounds on: Whitepaper §3/§5/§6/§6A/§6B, Sovereign‑Intelligence Vision (iCore/iQuorum/iCorp/membrane/lease), Track‑2 Functional Spec, Technical Spec, Node‑Packaging Addendum, and the built hinetd runtime (app/osx/hinetd/hinetd/).

Primitives reused verbatim: iCore (1 human's owned MoE) · iQuorum / iCorp (org‑gated rigid quorum) · qCore · membrane · consent / signed audit hash‑chain · competence‑signature routing · ComputeBackend / WorkOrder / BackendReceipt seam · ModelSpec roots (icore-generalist / icore-fast / icore-coding / icore-mini).

Status legend: [EXISTS] already built in hinetd · [NEW] to build · [MVP] in first cut · [Later] deferred. Central services (REGISTRY, network fan‑out) run on the Quorumz GCP project with their own dedicated resources for mind.quorumz.com / HiNet.

Central design rule (inherited): the funnel and harness live inside the owned node. Nothing crosses a membrane without a ConsentGrant; every privileged step appends to the audit hash‑chain; the loopback server binds 127.0.0.1 only (ALLOW_ORIGINS = loopback) [EXISTS].


PART A — First‑Run Onboarding / Download Funnel (in‑app)

A0. Where it lives

The funnel is a first‑run overlay served by hinetd's local web UI (webui/app.html, served at /app) and wrapped by the pywebview desktop shell [EXISTS: app.py] — later the Tauri shell (Node‑Packaging Addendum §A), which moves key custody into the Rust security core. The funnel is a state machine over hinetd endpoints, not a separate app.

First‑run detection [NEW]: the UI calls GET /node/onboarding on load. If state != "ready", it renders the funnel instead of the Ask/Sources tabs. The funnel is idempotent and resumable — closing the app mid‑download resumes at the same step (state is server‑side, see NodeState).

GET /node/onboarding  -> NodeState   # the single source of truth the UI renders

NodeState (computed, not just stored):

state: welcome | naming | downloading | download_failed | register_prompt | ready
node_name: "Eitan's MacBook" | null      # A3
model_plan: ModelPlan                     # A2 (always present — RAM check is instant)
download: DownloadStatus                  # A4 (state=downloading/failed only)
identity: { exists: bool, node_id: "icore_..." | null }
registration: { registered: bool, at: "..." | null }
resume_step: welcome|naming|downloading|register_prompt   # where to re-enter

A1. The funnel UX flow (text sequence)

STEP 0  WELCOME  (state=welcome)
  ├─ Ethos line (reuse /node/proof copy): "Your model runs on this Mac. Nothing leaves it."
  ├─ System check card, rendered from ModelPlan (GET /node/model/plan — instant, no download):
  │     "This Mac: 128 GB unified memory, Apple Silicon."
  │     "Your iCore: Qwen3-Next-80B-A3B-Instruct (MoE, 3B active) — ~45 GB, 4-bit."
  │     "Why this one: 64 GB+ machines get the largest MoE general model we ship; MoE is
  │      required so your iCore can later compose into a bigger quorum. (Smaller Macs get a
  │      smaller model automatically.)"
  │     Secondary line: coding root that will also be fetched (icore-coding).
  └─ [Continue]  ->  POST /node/onboarding/advance {to:"naming"}

STEP 1  NAME YOUR NODE  (state=naming)
  ├─ "Name this node. It's how your iCore appears when it joins the network."
  ├─ Text field, prefilled with a suggestion (hostname → "Eitan's MacBook").
  ├─ Validation: 1–64 chars, shown live; name is human-facing, non-unique (node_id is the unique key).
  └─ [Save & continue]  ->  POST /node/name {name}  ->  advance to "downloading"
        (Identity is minted here, lazily: POST /node/identity/init runs if no keypair yet — A6.)

STEP 2  DOWNLOAD  (state=downloading)
  ├─ On enter: POST /node/model/download {model:"icore-generalist"} (idempotent; also queues icore-coding)
  ├─ UI polls GET /node/model/status every ~1s and renders a live bar:
  │     "Downloading Qwen3-Next-80B-A3B — 12.4 GB / 45.1 GB (27%) · 58 MB/s · ~9 min left"
  │     per-file sublist optional (which shard is in flight).
  ├─ The owner can start using the app the moment icore-fast/mini is available (progressive readiness,
  │   see A5) — the big generalist keeps downloading in the background.
  └─ On complete  ->  advance to "register_prompt"

STEP 2'  FAILURE  (state=download_failed)
  ├─ Clear, honest error from DownloadStatus.error (e.g. "Network dropped at 61%.").
  ├─ Bytes already fetched are KEPT (HF cache is resumable). Copy: "We'll pick up where it stopped."
  ├─ [Retry] -> POST /node/model/download {resume:true}   (resumes from the last completed shard)
  ├─ [Switch to a smaller model] -> POST /node/model/plan/override {model:"icore-fast"} then retry
  │   (escape hatch when the machine/network can't sustain the largest tier)
  └─ Auto-retry with backoff happens server-side first (A5); the UI only surfaces failure after
     the retry budget is exhausted, so transient blips never bother the owner.

STEP 3  REGISTER INTO THE POOL  (state=register_prompt)
  ├─ "Your iCore is ready on this Mac. Register it so the network can find it — and pay you when it
  │   answers for someone else." (economy framing from Whitepaper §6A.)
  ├─ Shows: node name, node_id fingerprint (icore_ab12…), specialities auto-detected from roots
  │   (generalist + coding), endpoint (loopback for MVP; relay/tunnel later).
  ├─ Consent note: registration publishes ONLY {node_id, pubkey, name, capabilities, endpoint} to the
  │   REGISTRY — never data, never private weights. It is revocable.
  ├─ [Register]  ->  POST /node/register    (keypair → sign → POST to REGISTRY — A6)
  ├─ [Skip for now]  ->  advance to "ready" (a free, unregistered iCore per Vision §4; can register later
  │                       from Settings). Registration is NOT required to use your iCore.
  └─ On success  ->  advance to "ready"

STEP 4  READY  (state=ready)
  └─ Funnel dismisses; the normal Ask/Sources UI loads; first chat available immediately.

Open decision A‑i: does registration default on (opt‑out) or off (opt‑in)? The sovereign‑first ethos argues opt‑in with a strong nudge; the network‑effect argument (iCore Prime as "mining unit," §5) argues opt‑out. Recommend opt‑in + prominent nudge for MVP.

A2. RAM‑adaptive model selection — surface the existing logic

The selection itself already exists — config.py::_by_ram() / _ram_gib() pick the largest MoE tier the machine's hw.memsize supports, falling back to a small dense model [EXISTS]. What's missing is exposing it with a rationale to the funnel.

GET /node/model/plan  -> ModelPlan     # [NEW] — pure computation, instant, no download

ModelPlan schema [NEW]:

ram_gib: 128.0
apple_silicon: true
generalist:
  model: "icore-generalist"
  repo: "mlx-community/Qwen3-Next-80B-A3B-Instruct-4bit"
  display_name: "Qwen3-Next-80B-A3B-Instruct"
  role: "generalist"
  moe: true
  active_params: "3B"
  approx_download_gb: 45.1        # from HfApi model_info (A5), cached
  approx_resident_gb: 45          # weights; + KV/OS headroom
  tier_matched: 56.0              # the min-RAM tier the machine cleared
  reason: "64 GB+ Macs get the largest MoE general model; MoE lets your iCore compose bigger."
coding:
  model: "icore-coding"
  repo: "mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit"
  approx_download_gb: 17.0
fast_fallback:                    # what a low-RAM machine would get instead (shown as "why not bigger")
  repo: "mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit"
overrides: { env: ["HINETD_GENERALIST_REPO", ...], available_models: ["icore-fast","icore-mini"] }

Implementation: read _GENERALIST_REPO / _CODING_REPO from config.py; map the matched tier back to its reason string (add a parallel reason field to the tier tuples). Sizes come from HfApi().model_info(repo, files_metadata=True) summed over .safetensors, cached to ~/.hinet/model_sizes.json so the plan is instant offline.

POST /node/model/plan/override {model} [NEW, MVP] — pins a smaller root (writes HINETD_GENERALIST_REPO‑equivalent to node config) for the "switch to a smaller model" escape hatch.

A3. Node name + identity endpoints

GET  /node/name              -> {name, suggested}                       [NEW, MVP]
POST /node/name {name}       -> {name}                                  [NEW, MVP]
POST /node/identity/init     -> {node_id, pubkey, created_at}           [NEW, MVP]
GET  /node/identity          -> {exists, node_id, pubkey, fingerprint}  [NEW, MVP]

Open decision A‑ii: key recovery. The whitepaper flags "HiNet cannot recover lost keys" (Risk §8). MVP: emit a recovery phrase at identity init (BIP‑39‑style) that also unlocks VaultReplication restore; enforce "write it down" before leaving the naming step. [MVP]

A4. Model download endpoints + schemas

POST /node/model/download {model?, resume?:bool}  -> {started:true, model}   [NEW, MVP]
GET  /node/model/status[?model=icore-generalist]  -> DownloadStatus           [NEW, MVP]
POST /node/model/cancel {model}                   -> {cancelled:true}         [NEW, Later]

DownloadStatus schema [NEW]:

model: "icore-generalist"
repo: "mlx-community/Qwen3-Next-80B-A3B-Instruct-4bit"
state: idle | resolving | downloading | verifying | complete | failed
bytes_done: 13314000000
bytes_total: 45100000000        # from cached model_info; null while state=resolving
pct: 29.5
speed_bps: 58200000             # EMA over the last few polls
eta_seconds: 540
files: [ {name:"model-00003-of-00021.safetensors", pct:12, state:"downloading"}, ... ]  # optional
error: null | "network: connection reset (retry 3/5)"
attempts: 3
resumable: true                 # partial blobs on disk → Retry resumes, never restarts
updated_at: "..."

A5. The download engine (resumable, retry, live progress)

Today the model is fetched silently by the startup prefetch thread (server.py::_prefetch → reg.get → mlx_lm.load → huggingface_hub.snapshot_download) [EXISTS]. The funnel needs the same fetch instrumented and controllable. Build a DownloadManager [NEW]:

  1. Total size: HfApi().model_info(repo, files_metadata=True) → sum shard sizes → cache. (Offline/first‑paint: fall back to the approx_download_gb constant in ModelPlan.)
  2. Fetch: snapshot_download(repo, resume_download=True, max_workers=N) with HF_HUB_ENABLE_HF_TRANSFER=1 for throughput. resume_download=True is what makes Retry cheap — completed shards in the HF cache are skipped, so failure never restarts from zero.
  3. Progress: run the download in a daemon thread (as _prefetch already does); a poller thread computes bytes_done by summing sizes under the repo's blobs/ + *.incomplete files in ~/.cache/huggingface/hub/models--…, versus bytes_total. speed_bps = EMA of Δbytes/Δt; eta = (total-done)/speed. This is a real, buildable aggregate‑progress approach that needs no patching of huggingface_hub.
  4. Retry (server‑side, before surfacing failure): wrap the fetch in a bounded retry loop with exponential backoff (e.g. 5 attempts, 2ⁿ·jitter) — this is the "retry‑loop download learning" already prototyped in the E1 toolchain [EXISTS, adapt]. Only after the budget is exhausted does state=failed reach the UI. Each attempt increments attempts; the current attempt is shown in error so a slow retry never looks frozen.
  5. Verify: after complete, state=verifying → integrity check (HF already checksums blobs; optionally confirm config.json loads) → complete. Track‑2 §6.3 ("integrity verification").
  6. Progressive readiness: queue order is icore-fast/icore-mini (small, fast to land) → icore-generalist (large) → icore-coding. The moment any generalist root is loadable, /healthz.loaded reflects it and the Ask box works, while the big model streams in behind. This reuses the existing Registry lazy‑load + loaded_names() [EXISTS].

Integration with startup prefetch: keep HINETD_PREFETCH_MODEL [EXISTS], but route it through DownloadManager so a background prefetch and a funnel‑driven download share one job (no double download). POST /node/model/download is idempotent — if the prefetch already started it, the endpoint just returns the live DownloadStatus.

A6. Identity → sign → register sequence (into the global pool)

Registration publishes the node into the central REGISTRY (P‑I; own resources on the Quorumz GCP project). The node signs its own registration; the REGISTRY verifies the signature against the submitted pubkey (self‑certifying — the pubkey is the identity).

Node‑side endpoint [NEW, MVP]:

POST /node/register        -> RegistrationReceipt
GET  /node/registration    -> {registered, at, receipt}
POST /node/register/revoke -> {revoked:true}          [NEW, Later]

Text sequence flow:

1. UI POST /node/register
2. hinetd ensures identity exists (POST /node/identity/init if not) → {node_id, pubkey}
3. hinetd builds a RegistrationRequest (canonical JSON, sorted keys):
     { node_id, pubkey, name, capabilities, endpoint, icorp_binding?, nonce, issued_at }
4. hinetd signs canonical_bytes with the Ed25519 private key
     (MVP: in-process; Tauri: asks the Rust core to sign — key never enters Python).
5. hinetd POSTs { request, sig } to  REGISTRY  https://registry.mind.quorumz.com/v1/nodes/register
6. REGISTRY verifies sig over canonical_bytes with request.pubkey; rejects on mismatch/expired nonce.
7. REGISTRY upserts a RegistryNode row; returns RegistrationReceipt { node_id, registered_at,
     registry_sig }  (registry counter-signs so the node can prove it's listed).
8. hinetd stores the receipt, appends audit event `node_registered`, sets NodeState.registration.
9. UI advances to "ready".
Failure at 5/6 (offline, 4xx): NodeState stays register_prompt with a retry; the iCore is still fully
usable locally (registration is a network convenience, never a gate).

RegistrationRequest schema [NEW]:

node_id: "icore_ab12cd34..."
pubkey: "ed25519:base64..."
name: "Eitan's MacBook"
capabilities:
  roots: ["generalist", "coding"]           # from Registry.list() roles
  models: ["Qwen3-Next-80B-A3B", "Qwen3-Coder-30B-A3B"]
  specialities: []                            # [Later] competence-signature vector (P-J / M5-005)
  hardware_tier: "laptop"                     # pocket | laptop | prime | cloud
  attestation_level: "local"                  # from ComputeBackend.Capabilities
endpoint:
  kind: "loopback"                            # MVP: not publicly reachable — network calls are simulated/LAN
  # [Later] "relay" (signed-HTTP via a Quorumz relay) | "libp2p" | "tunnel"
icorp_binding: null                           # [Later] iCorp membrane + lease (Vision §3)
nonce: "..."
issued_at: "..."

RegistryNode (central record the REGISTRY stores) [NEW, central]:

node_id, pubkey, name, owner_ref?          # owner_ref only if the owner links an account (optional)
capabilities: {...}                         # as above; indexed for capability search (P-I)
endpoint: {...}
icorp_binding: null | { icorp_id, lease_active: bool }   # membrane/lease (Vision §3, §6B)
status: active | revoked
last_seen_at, registered_at
sig: "..."                                  # the node's registration signature (kept for audit)

Central REGISTRY endpoints (contract only — separate service, own GCP resources) [NEW, central, MVP‑thin]:

POST /v1/nodes/register        {request, sig}   -> RegistrationReceipt
POST /v1/nodes/revoke          {node_id, sig}   -> {revoked:true}
POST /v1/nodes/rotate-key      {node_id, new_pubkey, sig_old, sig_new} -> receipt   [Later]
POST /v1/nodes/heartbeat       {node_id, sig}   -> {ok, last_seen}                   [Later]
GET  /v1/discover?speciality=…&membrane=…       -> [RegistryNode]   # capability index (feeds P-J router)

Membrane rule (Vision §6B): an icorp‑bound node is not returned to public searches — only inside its iCorp workspace. Attribution/pay flow via the iCorp as a unit. [Later]

A7. PART A — MVP vs Later

Item Status
GET /node/onboarding state machine + first‑run overlay [NEW][MVP]
RAM‑adaptive selection [EXISTS] (config.py)
GET /node/model/plan + "why it fits" rationale [NEW][MVP]
Live download progress (/node/model/status, %/speed/ETA) [NEW][MVP]
Resumable retry (server backoff + Retry button) [NEW][MVP] (adapts E1 retry‑loop)
Startup prefetch (share one job) [EXISTS]
Name your node [NEW][MVP]
Ed25519 identity init (in‑process custody) [NEW][MVP]
Keychain key custody (Rust core) [NEW][Later] (Tauri)
Recovery phrase [NEW][MVP]
Register into pool (node side, sign+POST) [NEW][MVP]
Central REGISTRY (register/search) [NEW][MVP‑thin, central]
Key rotation / heartbeat / revoke / membrane‑gated search [NEW][Later]
Progressive readiness (small root first) [EXISTS‑ish][MVP]
Switch‑to‑smaller‑model override [NEW][MVP]

PART B — HiNet's Own Agent Harness (in‑process with hinetd)

B0. Philosophy

Build our own harness, in‑process with hinetd, borrowing three ideas but keeping the code ours (per master‑task‑list decision):

The harness is the concrete build of the Agent Layer (Functional Spec) and P6 "local Hermes" (Delivery Roadmap) — taking inspiration from Traia's cloud Hermes, not porting it.

What already exists [EXISTS]: POST /v1/agent runs a real multi‑step tool loop — apply_chat_template(tools=…)parse_tool_calls (JSON + Qwen‑Coder XML dialects) → execute_tool → feed results back, bounded by HINETD_AGENT_MAX_STEPS, serialized under _GEN_LOCK, with a trace (tools_used). Vault tools (search_memory/list_recent/list_channels) and repo‑confined dev tools (list_dir/read_file/search_code) exist. This is the harness kernel — PART B generalizes it into a plugin registry + ask‑modes.

B1. The agent loop (harness core)

Refactor the inline loop in server.py::agent_chat into a reusable Harness [NEW, MVP — mostly extraction]:

Harness.run(session) :
  1. Assemble system preamble: _situational_preamble(owner_accounts)  [EXISTS]
       + mode instructions (from the active AskMode plugin)
       + the tool manifests of the ENABLED plugins for this session.
  2. Loop up to max_steps (dev:10 / chat:6, env-overridable)  [EXISTS shape]:
       a. prompt = tok.apply_chat_template(messages, tools=enabled_tool_schemas, add_generation_prompt=True)
       b. out = generate (worker thread, under _GEN_LOCK)  [EXISTS]
       c. calls = parse_tool_calls(out)                    [EXISTS]
       d. if no calls -> final answer = strip_tool_calls(out); break
       e. for each call: result = registry.execute(call.name, call.args, ctx)   # plugin dispatch [NEW]
             append {role:"tool", ...}; append trace step   [EXISTS]
       f. enforce per-plugin consent + audit BEFORE execute (see B2.4)          [NEW]
  3. On step-budget exhaustion: force a final tool-less answer.   [EXISTS]
  4. Return AgentResult { answer, trace[], model, route? }.

Invariants kept from the current build: low temperature (0.3) for reliable tool‑calling; tools never raise (errors returned as text so the loop continues); Metal serialized under _GEN_LOCK; everything off the event loop via asyncio.to_thread.

AgentStep / trace schema [EXISTS, formalize]:

step: 1
plugin: "search_memory" | "read_file" | "ask_network" ...
arguments: {...}
result_preview: "…first 240 chars…"     # [EXISTS]
consent_grant_id: "…" | null            # [NEW] which grant authorized it
audit_event_id: "…"                     # [NEW] link into the hash-chain
sensitivity_redacted: bool              # [NEW] Sensitive/Secret redaction in visible trace

B2. Tool / plugin model ("everything is a plugin")

One registry, uniform manifest. Today tools live in two hardcoded lists (TOOL_SCHEMAS, DEV_TOOL_SCHEMAS) with an if is_dev_tool(...) dispatch [EXISTS]. Generalize to a PluginRegistry [NEW, MVP].

PluginManifest schema [NEW]:

name: "search_memory"
kind: tool | connector | ask_mode | router      # everything is a plugin
schema: { ...OpenAI/HF function-calling schema... }   # exactly today's TOOL_SCHEMAS shape [EXISTS]
group: "vault" | "dev" | "web" | "network" | "actions"
default_enabled: true
requires:
  consent_use: null | "agent_read" | "agent_write"   # maps to ConsentGrant.allowed_uses [EXISTS store]
  egress: false | true                               # off-device network → owner toggle, off by default
  confine_root: false | true                         # dev tools: realpath-confined to a chosen root [EXISTS]
side_effect: read | write                            # write/send/egress => just-in-time consent (B2.4)

Plugin contract (Python):

class Plugin:
    manifest: PluginManifest
    def execute(self, args: dict, ctx: HarnessContext) -> str: ...   # never raises; returns text

HarnessContext carries node() (vault/index/consent/audit), the session's enabled set, dev_root (if any), and ask_mode.

Registration [NEW]: built‑in plugins register at import (wrapping the existing execute_tool / execute_dev_tool); the registry exposes tool_schemas(enabled), execute(name, args, ctx), enable/disable(name). This is a thin adapter over code that already works — low risk.

Consent + audit gating (B2.4) [NEW, MVP]: before executing any plugin with side_effect: write or requires.egress, the harness calls the consent policy engine — a standing narrow pre‑authorization (ConsentGrant.allowed_uses contains agent_write) or a just‑in‑time approval surfaced to the UI (POST /v1/agent/approve {step_id, decision}), exactly the Track‑2 §5.3 model. Reads over already‑consented sources need no prompt. Every call — read or write — appends to the audit hash‑chain [EXISTS: audit.append]. The agent cannot self‑grant (the request_consent pseudo‑tool only asks).

MVP plugin set: vault (search_memory,list_recent,list_channels) [EXISTS] · dev (list_dir,read_file,search_code) [EXISTS] · request_consent [NEW]. Later: web_fetch/web_search (egress, off by default) · connector write tools (send_email, post‑to‑Slack — each agent_write‑gated) · dev write/run tools (edit_file, run_command — consent‑gated, currently read‑only by design).

B3. The three ask‑modes wired to the router (own / network / auto)

The ask‑mode is the routing scopewhich iCores answer. It is a plugin kind: ask_mode and is orthogonal to the existing execution modes in the UI (direct chat vs agent tool‑loop vs dev) [EXISTS]: execution mode = how the local model works; ask‑mode = where the intelligence comes from.

Extend the agent surface with an ask_mode field [NEW, MVP]:

POST /v1/agent { messages, model?, ask_mode: "own"|"network"|"auto", hinet_dev?, hinet_scope? }
   -> { answer, tools_used, model, route? }

(Backward compatible: absent ask_modeown, today's behavior.)

own [EXISTS, MVP] — the local iCore only (current /v1/agent). Fully offline, sovereign. This is the default and the only mode that needs no network.

network [NEW, MVP‑sim → Later‑real] — ask the pool. The harness gets an ask_network(question, speciality?, membrane?) plugin that:

1. Query REGISTRY /v1/discover?speciality=…  (membrane-aware; iCorp nodes hidden from public) [A6]
2. Select candidate iCores (competence-signature ranking — P-J).
3. Fan out the question over SIGNED HTTP to each node's registered endpoint (MVP transport;
   libp2p later). Each answering node runs its OWN /v1/agent(own) behind ITS consent.
4. Aggregate:
     - committee / MoA over returned texts  (cross-DNA; Whitepaper §3)   [MVP]
     - MoErging over public twins           (same-DNA, bigger single)    [Later]
5. Attach attribution: which iCores/iQuorumz/iCorps answered → pay-per-use split receipt.

MVP simplification: since MVP endpoints are loopback (not publicly reachable), network runs as a local simulation / LAN‑only fan‑out over registered dev nodes (Technical‑Spec "peer behavior as local simulation") — enough to build and test aggregation + attribution before real P2P transport lands. Real WAN transport is [Later] (P‑J: signed‑HTTP‑to‑registered‑endpoints, then libp2p).

auto [NEW, Later]right‑sizing (Whitepaper §2, §3.3): decide own‑vs‑network and how to combine, per the composition thesis "more isn't better." A RouteDecision plugin:

1. Estimate the question's contextual generality (narrow/personal → own; cross-domain → few experts;
   broad/general → escalate to a wider quorum).                              [competence-signature]
2. If narrow & the local iCore is competent -> route=own (cheapest, no egress). ["over-assembling loses"]
3. Else pick the minimal-sufficient set from REGISTRY; choose mode by fidelity-need × latency budget
   (merge complementary · route conflicting · retrieval-union for facts — the spectrum router).
4. Emit RouteDecision for the trace + attribution.

RouteDecision schema [NEW]:

mode: own | network
scope: personal | pair | team | domain | network
selected_nodes: ["icore_self", "icore_x…"]      # own => just self
aggregation: none | committee | moe_erging | retrieval_union
rationale: "narrow personal question — local iCore sufficient; escalation would only add cost"

UI wiring: the Ask box gains an ask‑mode selector (Own · Network · Auto) alongside the existing Direct/Agent/Dev mode chip [EXISTS]. For MVP, only Own is fully live; Network is behind a flag (LAN‑sim); Auto is stubbed to own until the router lands.

Open decision B‑i: collapse execution‑mode (direct/agent/dev) and ask‑mode (own/network/auto) into one control, or keep two? Recommend keeping two — they answer different questions and composing them (e.g. dev × network = "ask the network's coding iCores about my repo") is exactly the desired product surface.

B4. Dev‑mode tool integration

Two dev surfaces, both over the already‑OpenAI‑compatible hinetd [EXISTS: /v1/chat/completions, /v1/models, streaming].

B4.1 Point Cursor / VS Code at the local endpoint [EXISTS runtime + NEW docs/helper]

The runtime already serves an OpenAI‑compatible API meant for this (server.py docstring names Cursor). Deliver a one‑click config in the funnel/Settings ("Use my iCore in Cursor") that shows/writes:

Cursor → Settings → Models → OpenAI API:
  Base URL:  http://127.0.0.1:8765/v1
  API Key:   hinet-local        (any non-empty string; loopback is the auth boundary)
  Model:     icore-coding       (or icore-generalist)

VS Code (Continue / any OpenAI-compatible ext) → config:
  { "provider":"openai", "apiBase":"http://127.0.0.1:8765/v1",
    "model":"icore-coding", "apiKey":"hinet-local" }

GET /node/devtools/config?client=cursor|vscode -> {snippet, base_url, models} [NEW, MVP] renders these from live /v1/models. Caveat to surface honestly (Addendum §B): MLX prefill on long RAG contexts can be slow on pre‑M5 silicon — set expectations for large‑repo prompts.

B4.2 A Claude‑Code‑like terminal dev experience — hinet CLI [NEW, MVP]

A thin terminal client that drives POST /v1/agent in dev mode against the repo‑confined tools that already exist (list_dir/read_file/search_code, realpath‑confined, read‑only) [EXISTS]:

$ hinet                       # in a repo → dev agent rooted at CWD (realpath-confined)
> where is the audit hash-chain appended?
  �… searching code (search_code "audit") … reading app/osx/hinetd/hinetd/audit.py …
  The chain is appended in AuditLog.append() [audit.py:75] under _guard() …

$ hinet ask "summarize what changed in server.py this week"   # one-shot
$ hinet --model icore-generalist            # switch root
$ hinet --ask-mode network "how do others structure a plugin registry?"   # [Later] pool

CLI design (buildable now, ~a few hundred lines): - Resolves repo root = git top‑level or CWD; sends hinet_dev:{root} to /v1/agent [EXISTS param]. - Streams the trace live (tool calls + result_preview) — the Claude‑Code "watch it work" feel — from the existing tools_used trace [EXISTS]; upgrade /v1/agent to stream trace steps as SSE for real‑time display [NEW, MVP‑nice]. - REPL + one‑shot (hinet ask …) + flags (--model, --ask-mode, --max-steps, --json). - Read‑only by default (matches current dev‑tool safety invariants). Later: --allow-edit / --allow-run unlock consent‑gated edit_file/run_command write plugins (B2), each just‑in‑time approved and audited — the Claude‑Code edit/apply loop, kept sovereign. - Auth: loopback only; reuses the per‑launch bearer token once the Tauri core mints one [Later].

Why the CLI over /v1/agent (not a new engine): the agent loop, tool dispatch, confinement, and trace are all built. The CLI is a client; the harness is the server. This keeps one code path (the same loop powers UI chat, the CLI, and later network asks).

B5. PART B — key schemas summary

B6. PART B — MVP vs Later

Item Status
Multi‑step tool loop (kernel) [EXISTS]
Extract loop into reusable Harness [NEW][MVP]
PluginRegistry + PluginManifest (unify tool lists) [NEW][MVP]
Vault + dev tools as plugins [EXISTS→wrap][MVP]
Consent‑gate + audit every write/egress plugin; JIT approval [NEW][MVP]
ask_mode: own [EXISTS][MVP]
ask_mode: network (LAN/loopback simulation + committee aggregation) [NEW][MVP‑sim]
ask_mode: network real WAN transport (signed‑HTTP → libp2p) [NEW][Later]
ask_mode: auto (competence‑signature right‑sizing router) [NEW][Later]
Attribution receipts + pay‑per‑use split [NEW][Later]
Cursor / VS Code bridge (runtime) [EXISTS]
One‑click dev‑tool config helper [NEW][MVP]
hinet terminal CLI (read‑only dev agent) [NEW][MVP]
SSE trace streaming for the CLI/UI [NEW][MVP‑nice]
CLI write/run (--allow-edit/--allow-run, consent‑gated) [NEW][Later]
web_fetch / connector‑write plugins [NEW][Later]

B7. Harnessed learning — the harness can feed the sleep cycle [LATER]

Inspired by He et al., "Agent Lightning v1.0: Towards Harnessed Agentic RL" (arXiv:2608.17528, Aug 2026): the deploy-time agent harness can directly participate in model post-training via a disaggregated LLM-endpoint-proxy — the harness owns the environment-interaction loop and its traces become RL training signal (their result: +14.6 pts on SWE-bench Verified from only 6K episodes).

HiNet already has both halves: an OpenAI-compatible /v1 endpoint (the natural proxy point) and a nightly sleep cycle that does on-device post-training. The build-on is direct — instrument the harness's existing trace (tools_used, step outcomes, consent state) so successful owner-task episodes become on-device RL / preference signal for the sleep cycle's Δ_private update: harnessed agentic RL, but sovereign — the loop runs on the owner's machine over the owner's own interactions and never leaves the device (unlike the paper's centralized trainer). P6 / E2 stretch, gated on sleep-cycle training landing first (Personalization-Engine); the eval + leakage gate apply unchanged.

Open question we'd raise with the authors (see community notes): does harnessed agentic RL hold in the single-user, low-data, privacy-constrained regime (one person's daily tasks, no central trainer, DoRA-on-MoE), and can many per-human harnesses compose into a federated variant?

B8. MCP interop — make the iCore a mesh citizen [LATER]

The harness speaks OpenAI-compatible + our own tool dialect (B2). To interoperate with the wider agent ecosystem — and to ride an agent-mesh transport like SAM (Sovereign Agent Mesh) (Google, Apache-2.0: libp2p relay + crypto identity + MCP tool routing) — the harness should also speak MCP: expose the iCore's consent-gated tools as an MCP server, and consume remote tools as an MCP client. That lets an iCore be a node on a mesh (SAM or similar) without touching the composition layer, and is the interop path for ask_network (B3) over a real P2P substrate (Routing §4.2). Consent + audit gating (B2.4) applies to MCP calls unchanged; identity stays our self-certifying node_id (not a control-plane-issued one).


Cross‑cutting open decisions


This spec builds directly on the shipped hinetd runtime: it instruments the existing silent model prefetch into a visible, resumable funnel; adds the identity/registry module the Technical Spec scoped but hadn't built; and generalizes the existing /v1/agent tool loop into a plugin‑based harness with the three ask‑modes and a Claude‑Code‑style dev CLI — everything local‑first, consent‑gated, and audited, per the HiNet sovereignty invariants.


Next: Owner identity & proof-of-personhood →  ·  All documentation →