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]
- Name persists in a new
node_metarow in the vault (key='node_name'), audited (node_named). - Identity
[NEW]: generate an Ed25519 keypair (cryptography/PyNaCl); thenode_idis derived from the pubkey per the authoritative Node-Identity & Registry §1.2 (P-I) formula (icore_ + crockford_base32(sha256(0x01‖pubkey)[:20])→icore_+ 32 crockford-base32 chars), not restated divergently here. Private‑key custody: - MVP (pywebview): store the private key sealed in the vault / a
0600file under~/.hinet/(SQLCipher hardening path already planned). - Product (Tauri): key lives in macOS Keychain, minted/held by the Rust security core (Addendum §A — "Ed25519 identity, private key in Keychain"); hinetd only ever receives signatures, never the private key. The endpoint contract is identical; only the custody backend swaps.
- Matches Technical‑Spec
node-identitymodule +NodeProfilecontract and Track‑2 §6.2. This is the first concrete build of that module (only the model registry exists today, not the identity one).
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]:
- Total size:
HfApi().model_info(repo, files_metadata=True)→ sum shard sizes → cache. (Offline/first‑paint: fall back to theapprox_download_gbconstant inModelPlan.) - Fetch:
snapshot_download(repo, resume_download=True, max_workers=N)withHF_HUB_ENABLE_HF_TRANSFER=1for throughput.resume_download=Trueis what makes Retry cheap — completed shards in the HF cache are skipped, so failure never restarts from zero. - Progress: run the download in a daemon thread (as
_prefetchalready does); a poller thread computesbytes_doneby summing sizes under the repo'sblobs/+*.incompletefiles in~/.cache/huggingface/hub/models--…, versusbytes_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. - 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 doesstate=failedreach the UI. Each attempt incrementsattempts; the current attempt is shown inerrorso a slow retry never looks frozen. - Verify: after complete,
state=verifying→ integrity check (HF already checksums blobs; optionally confirmconfig.jsonloads) →complete. Track‑2 §6.3 ("integrity verification"). - 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.loadedreflects it and the Ask box works, while the big model streams in behind. This reuses the existingRegistrylazy‑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):
- DeepSeek‑Harness — "everything is a plugin." Tools, connectors, ask‑modes, and even the router are plugins registered into one registry with a uniform manifest. Adding a capability = dropping a plugin, never editing the loop.
- OpenClaw — a lean, model‑agnostic agent loop over the OpenAI‑compatible surface. The anchor stays Qwen; the loop drives any root.
- Claude Code — the terminal dev experience: a tight read→search→edit tool loop with a trace the user can watch, plus IDE bridging.
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 scope — which 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_mode ⇒ own, 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
PluginManifest[NEW]— uniform descriptor for tools/connectors/ask‑modes/routers.AgentStep/ trace[EXISTS→formalize]— +consent_grant_id,audit_event_id, redaction flag.AskRequest=/v1/agentbody +ask_mode[NEW field].RouteDecision[NEW]— theautorouter's output; feeds trace + attribution.AttributionReceipt[Later]— which iCores/iQuorumz/iCorps answered + pay‑per‑use split (Vision §7, Whitepaper §6A); attaches to theWorkOrder/BackendReceiptenvelope[EXISTS seam].
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
- A‑i Registration default: opt‑in + nudge (recommended) vs opt‑out network‑effect.
- A‑ii Key recovery: BIP‑39 recovery phrase at init, tied to
VaultReplicationrestore (recommended MVP). - A‑iii REGISTRY hostname + auth model (self‑certifying pubkey vs also linking an owner account) — recommend pubkey‑only self‑cert for MVP.
- A‑iv Endpoint reachability for real
networkasks: relay via Quorumz (fast) vs libp2p (decentralized) — MVP simulates, decide before P‑J. - B‑i Keep execution‑mode (direct/agent/dev) and ask‑mode (own/network/auto) as two controls (recommended) vs merge.
- B‑ii Router placement: run the
autocompetence‑signature router locally on the node, or as a REGISTRY‑side service? Local keeps sovereignty; central scales indexing. Recommend local decision over a central capability index. - B‑iii Private‑key signing boundary in MVP (in‑process Python) vs waiting for the Tauri Rust core — recommend in‑process for MVP with an identical endpoint contract so the swap is invisible.
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 →