forgo.cloud
Sign in
Repo workspace

forkjoin-ai/gnosis

Zedge Consumer Map: wiring the upstream editor to the guarded subagent swarm

distributed-inference-host/ZEDGE_CONSUMER_MAP.md
forkjoin-ai/gnosis

Zedge Consumer Map: wiring the upstream editor to the guarded subagent swarm

Status: analysis / proposal only. No zedge code is changed by this document. Zedge (open-source/zedge) is the UPSTREAM CONSUMER and is treated read-only. Section 5 documents the new first-class forkjoin passthrough tier (in-progress, implemented by a sibling); the contract there is agreed but the source is not yet verified by this document -- items are flagged "to confirm in reconciliation".

This maps how zedge launches local inference today to how it should consume the new producer-side primitives in open-source/gnosis:

  • monster-swarm — UCAN-gated, cost-admitted CRUD over a leased subagent fleet.
  • monster-resident — the MCP server that surfaces that CRUD as the subagent_* / swarm_* tools (plus orchestrate).
  • monster-guard — the runtime sandbox + lease + revoke enforcer.
  • moonshine — the default node (Qwen-knot fat-station warmer).
  • fat-station — the dense/rknot inference station the Paris quality gate runs.

0. Producer-side facts established by reading the source

Verified against the producer tree (not assumed):

  • Default node when none is given is "moonshine", overridable per-call (node arg) or globally via MONSTER_SWARM_DEFAULT_NODE. monster-swarm/src/ops.rs:28-32.
  • Spawn is double-gated: UCAN agent/spawn (or agent/*) on agent:fleet rooted at the resident root (ops.rs:61-90), then AntColony FRF admission (ops.rs:149-154), then monster-guard delegate + monster-guard run (guard.rs:98-163). Nodes must resolve inside the allowlist (MONSTER_SWARM_NODES / DI_BIN_DIR / DI cargo build dirs) — guard.rs:35-95.
  • MCP tool contract is exact: subagent_create takes { ucan, node?, id?, caste?, caps?, lease_secs?, max_secs?, grant_ttl_secs?, node_args? }, only ucan required; subagent_list/subagent_reconcile/swarm_vibe need no UCAN; subagent_update/subagent_reap/swarm_autotick require UCAN. monster-resident/src/main.rs:331-437, 570-681.
  • Per-subagent identity: spawn mints a DID + 0600 keyfile and hands the node MONSTER_AGENT_ID, MONSTER_AGENT_DID, MONSTER_AGENT_KEYFILE (ops.rs:160-168). Shared state lives under $MONSTER_GUARD_HOME (default ~/.moonshine.d).
  • The verified Qwen Paris quality gate spawns fat-station (NOT moonshine) as a leased Scout with caps=["net"], node_args = --knot <dense> --port 8779 --role both --layers 0..28, drives /decode-next over the wire, asserts next token 12095 (" Paris"), then reaps. monster-swarm/tests/paris_subagent.rs. The knot it uses is ~/.edgework/models/qwen-coder-7b.knot (a coder model); the test deliberately asserts the token-level completion (token 12095) of "The capital of France is", because a coder model deflects chat questions ("I'm a coding assistant…"). This is the chat-template deflection risk (see §4).
  • Built binaries present locally: monster-resident (release + debug), fat-station (release), monster-guard. NOT present: fat-station-memo release, and any qwen2.5-0.5b-instruct-q4_k_m.knot (see §2 gap).

1. How zedge SHOULD spawn its inference as a UCAN-leased subagent

1a. What zedge does today (the unguarded path)

zedge/companion/src/moonshine-docker.ts is the entire launch path:

  • ensureMoonshineRunning() (:974) probes :8080 (OpenAI shim) and :8000 (fat-station), and if not healthy, calls startLocalMoonshine() (:733).
  • startLocalMoonshine() directly spawnDetacheds the fat-station binary (FAT_STATION_BIN, preferring fat-station-memo, falling back to fat-station) with args built by buildFatStationSourceArgs() (:667):
    • rknot mode: --rknot <rknot> --dense <knot>
    • dense mode: --knot <knot> plus --port 8000 --role both --layers <range> and the GNOSIS_* env knobs (:773-804).
  • Then it spawnDetacheds the TypeScript OpenAI shim (distributed-inference-host/src/bin/openai-server.ts) on :8080 with FAT_STATION_URL, MODEL_NAME, AGENTIC=0, memo flags, optional TOKENIZER_GGUF_PATH (:884-908).
  • A watchdog re-runs ensureMoonshineRunning() on degrade (:1034).

There is no UCAN, no lease, no sandbox, no revoke, no population ceiling. The fat-station runs as a bare detached child of the companion. This is exactly the gap monster-swarm/monster-resident close.

1b. The target call sequence (guarded)

Replace the bare spawnDetached(FAT_STATION_BIN, …) with a guarded spawn that goes through monster-resident. Two integration shapes; recommend B for the companion sidecar.

Shape A — direct over MCP stdio (mirrors run-mcp-with-supervisor.sh). The companion already speaks MCP stdio (mcp-stdio.ts). Add a second outbound MCP client that launches monster-resident and calls its tools:

  1. Provision once (out of band, by the operator):

    export MONSTER_GUARD_HOME=~/.moonshine.d
    monster-resident whoami                 # the root DID; must root every cap
    # mint the fleet capability for the companion's station DID:
    monster-resident grant \
      --audience <companion-station-did> \
      --with agent:fleet --can agent/* --caps net --ttl 86400  > fleet.ucan

    Note: grant --with agent:fleet --can agent/* is the swarm capability the subagent_* tools verify (ops.rs:78-89). (The README example grant --with plugin:<name> is for orchestrate, a different surface.)

  2. Launch the resident with the node allowlist + guard wired:

    MONSTER_GUARD_HOME=~/.moonshine.d \
    MONSTER_GUARD_BIN=<…>/monster-guard/target/release/monster-guard \
    MONSTER_SWARM_NODES=<…>/distributed-inference/target/release \
    DI_BIN_DIR=<…>/distributed-inference/target/release \
    monster-resident serve            # stdio JSON-RPC
  3. From the companion, MCP initialize, then tools/call subagent_create:

    { "name": "subagent_create", "arguments": {
        "ucan": "<fleet.ucan>",
        "node": "fat-station",          // the runnable dense station
        "id": "zedge-inference",
        "caste": "breeder",             // persistent: it's the editor's model host
        "caps": ["net"],
        "lease_secs": 60,
        "grant_ttl_secs": 86400,
        "node_args": [
          "--knot", "<~/.edgework/models/qwen-coder-7b.knot>",
          "--port", "8000", "--role", "both", "--layers", "0..28"
        ]
    }}

    This is the Paris-gate node line, parameterized. The node serves on :8000 exactly as moonshine-docker.ts expects, so the existing OpenAI shim step is unchanged and still binds FAT_STATION_URL=http://127.0.0.1:8000.

  4. Health-wait on :8000/health (zedge already has waitUrlReady), then start the shim on :8080 as today.

  5. On shutdown / model switch: tools/call subagent_reap { ucan, id } — the node dies at the next lease tick. The companion watchdog calls subagent_reconcile (no UCAN) to prune zombie rows, and subagent_list to read fleet state instead of port-probing.

Shape B — wrap monster-swarm spawn inside the existing startLocalMoonshine() (recommended). Keep moonshine-docker.ts as the orchestrator, but swap the fat-station spawnDetached for an MCP subagent_create call (or, simplest, shell out to the monster-swarm CLI which shares the same ops core). The OpenAI-shim spawn, health probes, model fingerprint checks, and watchdog all stay. This is the minimum-blast-radius change: only buildFatStationSourceArgs + the two spawnDetached(FAT_STATION_BIN…) sites move behind the guard.

1c. Concrete env/knot/bin mapping

zedge concept (moonshine-docker.ts) guarded equivalent
FAT_STATION_BIN (fat-station-memo/fat-station) node: "fat-station" resolved by MONSTER_SWARM_NODES/DI_BIN_DIR allowlist
buildFatStationSourceArgs()--knot/--rknot --dense node_args: [...] (same flags, byte-for-byte)
--port 8000 --role both --layers <range> identical, passed through node_args
spawnDetached(... detached:true) monster-guard run (lease + sandbox + revoke) via subagent_create
watchdog ensureMoonshineRunning repair subagent_list + subagent_reconcile, respawn on dead row
(none) — no auth today UCAN agent/* on agent:fleet, rooted at resident root
(none) — no kill switch subagent_reap (revoke)
ZEDGE_FAT_STATION_BIN env override still works for choosing the binary name passed as node
GNOSIS_NUM_THREADS, GNOSIS_FFN_*, RUST_BACKTRACE inherited by the node (env inheritance — see §4 risk), set on the resident process

The OpenAI shim (:8080) and Zed's openai_compatible provider pointing at 127.0.0.1:7331/v1 are unchanged. Only the fat-station birth is moved behind the guard.


2. Knot / model config reconciliation (the central gap)

The gap

What Path Exists?
zedge's expected dense knot open-source/bitwise/datasets/qwen2.5-0.5b-instruct-q4_k_m.knot (moonshine-docker.ts:31-34) NO — bitwise/datasets/ has no .knot files
zedge's expected tokenizer GGUF …/gguf/qwen2.5-0.5b-instruct-q4_k_m.gguf (:35-38) NO
zedge model id (catalog + Zed picker) qwen2.5-0.5b-instruct (model-catalog.ts:30-34, moonshine-docker.ts:55) maps to the missing knot
the ONLY runnable dense knot ~/.edgework/models/qwen-coder-7b.knot (4.7 GB, cached) YES
moonshine node default falls back to ~/.edgework/models/qwen-coder-7b.knot, else the same missing qwen2.5-0.5b-instruct-q4_k_m.knot path (moonshine/src/main.rs:112-126) runnable only via the cached coder knot

So zedge defaults to a 0.5B-instruct knot that is neither on disk nor in R2, while the producer (moonshine + the Paris gate) has already standardized on the 7B coder dense knot that is cached. The 0.5B path's R2 fallback also does not exist — only gemma4-31b-it.knot has a real R2 URL in zedge (moonshine-docker.ts:43-45); there is no qwen…0.5b R2 URL at all.

Align zedge's runnable Qwen model with the cached coder knot the producer already verifies:

  • Point the Qwen spec at ~/.edgework/models/qwen-coder-7b.knot (matching moonshine/src/main.rs precedence and the Paris gate), OR, cleaner, add a new catalog entry qwen-coder-7b whose knotPath is the cached file and make it the local default.
  • Until a real qwen2.5-0.5b-instruct-q4_k_m.knot is produced and uploaded to R2, do not advertise qwen2.5-0.5b-instruct as a local model (it will fail the sourceExists() check at moonshine-docker.ts:222-230 and silently fall back). Either gate it behind R2 availability or relabel.
  • The coder-7B is a base/coder model: keep AGENTIC=0 and prefer completions (/v1/completions, already wired for edit-predictions in generate-settings.ts:192-196) over chat for FIM/tab use, where token-level completion (the Paris-gate behavior) is exactly what you want.
  • The runtime fingerprint guard in moonshine-docker.ts (hidden_dim, vocab_size, layers) means the layer range must match what the node reports. The Paris gate uses --layers 0..28 for the 7B; zedge's Qwen spec carries no defaultLayers (so it derives 0..<num_layers> from knot metadata, or falls back to 0..22DEFAULT_KNOT_LAYER_COUNT). Set the Qwen-coder spec's defaultLayers to the coder knot's true depth (28 per the gate) to avoid the fingerprint mismatch restart loop.

3. PROPOSED minimal edits to zedge (checklist — DO NOT APPLY)

All edits are in open-source/zedge/companion/. Marked PROPOSED; for review.

  • PROPOSED companion/src/moonshine-docker.ts:31-38 — repoint the Qwen spec to the runnable knot. Either change QWEN_KNOT_PATH to ~/.edgework/models/qwen-coder-7b.knot (with existsSync precedence kept) or add a QWEN_CODER_KNOT_PATH constant pointing there.
  • PROPOSED companion/src/moonshine-docker.ts:157-178 (LOCAL_MOONSHINE_MODELS) — add a qwen-coder-7b spec { modelName, knotPath: cached, defaultLayers: '0..28' }; optionally make it the local default instead of gemma4-31b-it at :284.
  • PROPOSED companion/src/model-catalog.ts:16-41 — add a qwen-coder-7b entry to KNOWN_ZEDGE_MODELS (and consider demoting qwen2.5-0.5b-instruct until its knot exists in R2).
  • PROPOSED companion/src/moonshine-docker.ts:733-911 (startLocalMoonshine) — replace the two spawnDetached(FAT_STATION_BIN, [...buildFatStationSourceArgs(config), …]) blocks (:773 and :845) with a guarded spawn: an MCP subagent_create to a monster-resident client (Shape A) or a monster-swarm spawn shell-out (Shape B). Keep the OpenAI-shim spawn (:883-908) and all health probes.
  • PROPOSED new file companion/src/monster-resident-client.ts — a small MCP stdio client (initialize → tools/call) that launches/connects to monster-resident serve and exposes subagentCreate/list/reap/reconcile. Reuse the existing stdio framing from mcp-stdio.ts.
  • PROPOSED new env in moonshine-docker.ts resolution block (near :57-120): ZEDGE_MONSTER_RESIDENT_BIN, ZEDGE_MONSTER_GUARD_BIN, ZEDGE_MONSTER_SWARM_NODES (→ MONSTER_SWARM_NODES/DI_BIN_DIR), ZEDGE_FLEET_UCAN (or a path to fleet.ucan), MONSTER_GUARD_HOME (default ~/.moonshine.d).
  • PROPOSED companion/src/moonshine-docker.ts:1034-1066 (startMoonshineRuntimeWatchdog) — on degrade, prefer subagent_reconcile + subagent_list over raw port probing, and respawn via the guarded path.
  • PROPOSED scripts/companion-launch-agent.sh:249-265 (plist EnvironmentVariables) — add MONSTER_GUARD_HOME, MONSTER_GUARD_BIN, MONSTER_SWARM_NODES/DI_BIN_DIR so the launchd-managed companion can reach the guard + node allowlist (launchd has no shell profile).
  • PROPOSED docs only: scripts/generate-settings.ts — no functional change needed; the 127.0.0.1:7331/v1 provider and /v1/completions edit-prediction wiring stay. Optionally update the model list comment to mention qwen-coder-7b.
  • PROPOSED (operator runbook, not code) — document the one-time monster-resident grant --with agent:fleet --can agent/* provisioning and where fleet.ucan is stored (suggest ~/.edgework/fleet.ucan, 0600).

Smallest honest first step: do the §2 knot repoint (catalog + spec) so local inference works at all with the cached knot; then layer in the guarded spawn (Shape B) so it works safely.


4. Mismatches / risks

  • Auth model mismatch. zedge's local auth is an OpenAI-compatible bearer key (ZEDGE_API_KEY / zedge-local, companion-launch-agent.sh:49,257-262) that protects the :7331/:8080 HTTP surface. monster-swarm auth is a UCAN (agent/* on agent:fleet) that protects the spawn surface. These are orthogonal and both needed: the API key gates who can ask for inference; the UCAN gates who can create/kill inference nodes. Do not conflate them. zedge has a UcanBridge (companion/src/ucan-bridge.ts) but it is rooted at zedge's own identity for collab/file caps — the fleet capability must be rooted at the monster-resident root DID (monster-resident whoami), a different root.
  • Knot does not exist (highest-impact). The default Qwen knot zedge points at (qwen2.5-0.5b-instruct-q4_k_m.knot) is absent on disk and in R2; only the 7B coder knot is runnable. Without the §2 repoint, the guarded path will spawn a node that fails sourceExists() and the companion silently degrades.
  • Coder-vs-instruct behavior + chat-template deflection. The runnable knot is a coder model. On /v1/chat/completions it will deflect chat-style questions ("I'm a coding assistant…"); the producer's own quality gate works around this by asserting the raw next token, not chat output (paris_subagent.rs:5-9, 30-33). Implication for zedge: route the local coder-7B to completions/FIM (tab predictions, already wired) and treat the Agent-panel chat experience as second-class until a true instruct knot exists. Keep AGENTIC=0 for the shim (already the default at moonshine-docker.ts:890).
  • Ports. No conflict introduced: fat-station stays :8000, OpenAI shim :8080, companion :7331, Paris-gate uses :8779 (test-only). The guarded node must still be told --port 8000 via node_args so the shim's FAT_STATION_URL resolves. The resident itself uses stdio, no port.
  • Layer-range fingerprint loop. zedge restarts the listener when the runtime fingerprint (hidden_dim/vocab/layers) mismatches the expected spec (moonshine-docker.ts:452-470, 820-837). If the Qwen-coder spec's defaultLayers is left unset (derives 0..22) but the node serves 0..28, the companion will loop restarting. Set defaultLayers: '0..28' for the coder spec (per §2/§3).
  • fat-station-memo absent. zedge prefers fat-station-memo and falls back to fat-station (moonshine-docker.ts:60-78); only fat-station is built in release. The allowlist (MONSTER_SWARM_NODES) must contain the directory of whichever binary is chosen; if zedge passes node: "fat-station-memo" it must exist in the allowlist or resolve_node rejects it (guard.rs:57-95).
  • Node allowlist is secure-by-default (fails closed). If neither MONSTER_SWARM_NODES nor DI_BIN_DIR is set, no node resolves and every spawn errors no node allowlist configured (guard.rs:65-69). The launchd plist and any sidecar env MUST set it explicitly — launchd does not inherit the shell profile.
  • Environment inheritance. Spawned nodes inherit the launcher's env (monster-swarm SECURITY.md "Environment inheritance"). The companion holds ZEDGE_API_KEY/OPENAI_API_KEY in its plist env; the leased fat-station doesn't need them. Run the resident with a minimal env, or accept that the node sees them (the keys are local placeholders, low severity, but worth noting).
  • Single-writer ledger. monster-swarm assumes one writer to fleet.json (atomic but not lock-protected). If both the companion and an operator drive monster-swarm concurrently, an update can be lost. Keep the companion the sole driver in the running session.
  • macOS sandbox only enforces signed binaries. Unsigned allowlisted nodes run without the seatbelt profile (lease + revoke still apply). The local fat-station is likely unsigned, so the sandbox dimension is advisory until the node binaries are signed; the lease/revoke kill switch is real regardless.

5. First-class forkjoin passthrough tier (new, in-progress)

Status: contract agreed; sibling (ZW1) is implementing the source. The points below describe the agreed contract, not yet-verified code. Items marked "to confirm in reconciliation" depend on the sibling's final implementation.

What it is

A new first-class inference tier named 'forkjoin' added to companion/src/inference-bridge.ts. It forwards OpenAI-shaped chat-completion requests straight THROUGH to the Forkjoin OWN distributed-inference mesh (fat-station / knots / WASM worker, surfaced via @a0n/distributed-inference-host). This is the project's own runtime: there is NO third-party and NO paid inference involved. It is the same mesh the rest of this document maps; the forkjoin tier is simply the direct OpenAI-compatible passthrough into it from zedge's hotpath.

Where it sits in the hotpath

infer() in inference-bridge.ts is the tier chain. Today (baseline) the chain is:

Tier 1: moonshine -> Tier 2: echo (guaranteed fallback)

With the forkjoin tier wired in, the order becomes:

forkjoin (PRIMARY) -> moonshine (graceful fallback) -> echo (guaranteed last)
  • forkjoin is tried first. On HTTP error, timeout, or any failure it does NOT throw the whole request: it falls through to Moonshine exactly as Moonshine today falls through to echo.
  • moonshine keeps its current role as the local-container fallback.
  • echo remains the final belt-and-suspenders response so the editor always gets a reply.

The InferenceTier union (currently 'mesh' | 'edge' | 'cloudrun' | 'wasm' | 'echo' | 'moonshine') gains 'forkjoin'. The X-Zedge-Tier response header and the attempts[] chain (TierAttempt / TierResult) report forkjoin like any other tier, so the existing telemetry and SSE-proxy chain logging cover it without change.

Config (env-gated)

Env var Default Meaning
ZEDGE_FORKJOIN_ENABLED on (default) Master switch for the forkjoin tier. When off, the chain reverts to moonshine -> echo.
ZEDGE_FORKJOIN_URL own-mesh base The OWN-runtime mesh OpenAI-compatible base URL the passthrough targets (local default is a loopback mesh endpoint).
forkjoin timeout to confirm Per-attempt timeout; on expiry the tier records timeout and falls through to moonshine. Exact env name/default: to confirm in reconciliation.

When ZEDGE_FORKJOIN_ENABLED is off OR ZEDGE_FORKJOIN_URL is unset/empty, the forkjoin tier is skipped and the hotpath behaves exactly as the moonshine -> echo baseline (no behavior change for existing setups).

Request / response shape

OpenAI /v1/chat/completions passthrough. The same ChatCompletionRequest the companion already builds (post engram-injection and system-prompt budgeting) is forwarded to ${ZEDGE_FORKJOIN_URL}/v1/chat/completions. Both streaming (SSE) and non-streaming responses are proxied back unchanged, matching the moonshine path's response handling. No request reshaping beyond what the companion already does for the existing chat path is required, because the mesh endpoint is OpenAI-compatible (it is the same shim contract documented in sections 1-4: fat-station behind the distributed-inference-host OpenAI server).

Auth: local loopback vs guarded production

  • Local / development path: loopback, no-auth. ZEDGE_FORKJOIN_URL points at a loopback OWN-mesh endpoint on the same host; no UCAN and no bearer key are required for the passthrough itself (the same posture as the local Moonshine container today).
  • Guarded production path: requests go through UCAN / monster-resident, i.e. the leased, revocable, cost-admitted mesh access this document describes in sections 1-4. The passthrough target is then the guarded mesh surface rather than a bare loopback port. The exact wiring of the production UCAN root and whether the forkjoin tier mints/attaches the capability itself or relies on an already-provisioned fleet.ucan: to confirm in reconciliation.

Note (consistent with section 4): the OpenAI-key surface that protects zedge's :7331/:8080 HTTP listener and the UCAN that gates mesh node lifecycle are orthogonal; the forkjoin passthrough is a request-time data-plane call into the mesh and does not change the spawn-time UCAN model.

Relationship to the rest of this map

Sections 1-4 cover the BIRTH AND DEATH of the mesh node (guarded spawn / lease / reap of the fat-station). The forkjoin tier covers the REQUEST PATH into that already-running mesh. They compose: monster-swarm / monster-resident lease the node; the forkjoin passthrough is how zedge's infer() sends OpenAI traffic to it as the primary tier, with moonshine and echo as the graceful and guaranteed fallbacks.


  1. Fix the model first (§2). Repoint zedge's Qwen spec from the missing qwen2.5-0.5b-instruct-q4_k_m.knot to the cached, runnable ~/.edgework/models/qwen-coder-7b.knot, add a qwen-coder-7b catalog + spec entry with defaultLayers: '0..28', and stop advertising the 0.5B knot as a local model until it exists in R2.
  2. Guard the spawn (§1, Shape B). Inside startLocalMoonshine, replace the bare spawnDetached(FAT_STATION_BIN, …) of the fat-station with a guarded spawn through monster-resident's subagent_create (or a monster-swarm shell-out): node: "fat-station", caste: "breeder", caps: ["net"], node_args = the existing --knot/--rknot flags + --port 8000 --role both --layers 0..28. Keep the OpenAI shim, health probes, and watchdog; route teardown through subagent_reap and liveness through subagent_list / subagent_reconcile.
  3. Provision the capability (§1b, operator). monster-resident grant --with agent:fleet --can agent/* --caps net, rooted at the resident root, stored as ~/.edgework/fleet.ucan (0600); wire MONSTER_GUARD_HOME, MONSTER_GUARD_BIN, and MONSTER_SWARM_NODES/DI_BIN_DIR into the companion env (incl. the launchd plist).
  4. Mind the model character (§4). The runnable knot is a coder model — route it to completions/FIM, keep AGENTIC=0, and expect chat deflection until a true instruct knot is produced.

Net: the API-key surface is unchanged; only the birth and death of the fat-station moves behind the UCAN + lease + allowlist, turning zedge's bare detached child into a leased, revocable, cost-admitted subagent.