Code agents without a host filesystem — JS bodies through the substrate (loomcycle v0.19.0 + v0.20.0) Loomcycle v0.19.0 and v0.20.0 remove the host filesystem dependency for JavaScript code agents by introducing inline code bodies in AgentDef, enabling portable deployment across cloud, orchestration, and n8n environments. The update includes content-addressed hashing, a 256 KB cap, and fixes for validation, caching, and deduplication, while v0.20.0 adds Web UI support for code bodies. Code-as-agent RFC J, v0.16.0 shipped operator JavaScript as a first-class agent via goja, with the JS body read from agent code//index.js on the loomcycle sidecar's disk. Every OTHER AgentDef attribute system prompt, allowed tools, tier, model had been content-addressed, versioned, and snapshot-portable through the dynamic substrate since v0.9. The JS body alone wasn't — and that host-FS dependency didn't survive three deployment shapes real operators were already running: pure cloud no host filesystem to bind into a loomcycle container , container orchestration where bind-mounting the host disk breaks the portability story, and n8n interactive where workflow authors define agents at design time and never touch the sidecar's disk. v0.19.0 PR 349 + follow-up 350 threads inline code body through AgentDef as a hash-significant content field. Empty body omitempty's out of canonicalization, so every existing non-code AgentDef hashes byte-for-byte as before — zero upgrade churn. Inline code is content: two bodies hash differently, verify/dedup stays correct. The field rides through four mirrors config.AgentDef.Code yaml:code, mergedDef, lookup.SubstrateAgentDef + ToConfigDef, AgentContent and providers.RunMeta.CodeBody from the loop to the provider, with the leaf providers package keeping its no-store/no-tools invariant. All four run-creation paths RunOnce, /v1/runs, /v1/sessions messages-continuation, runSubAgent populate it so sub-agents inherit the body identically. Provider prefers inline body over filesystem; the compiler cache re-keys by content sha256 not by agent name so a new AgentDef version with new JS compiles fresh instead of serving a stale program. Gated via the existing LOOMCYCLE CODE AGENTS ENABLED switch; dedicated 256 KB cap LOOMCYCLE AGENT DEF MAX CODE BYTES ; pure codejs.Validate as single source of truth for parse-compile cycle-free . Forking a filesystem-backed code agent into the substrate is intentionally refused with a hint to supply inline code body — auto-materialising the disk body would re-introduce the FS dependency we're removing. Three review-fixes in the same PR: 1 boot-fatal validation had only known about the filesystem, so the headline no-FS-bind case was failing log.Fatalf at boot; now validates via codejs.Validate when def.Code is set. 2 Per-turn disk read regression on the FS path — the compiler refactor for inline bodies accidentally dropped load 's by-name early check, so every replay turn of a code-agent run was re-reading + re-hashing index.js; restored a by-name fsCache for the filesystem path while keeping the inline path content-hash-keyed a by-name cache there would serve a stale program after promotion . 3 False dedup contract — AgentDef.execCreate had no content-addressed dedup unlike MCPServerDef, so the TS ensureCodeAgent "changed" flag was always true and its byte-identical re-register no-op contract was a lie; added the same idempotent-create dedup MCPServerDef got in 343. PR 350 closes a three-way hash drift the v0.19 work surfaced: FromYAMLAgent omitted CodeBody, agents.Agent had no Code field, so the .md-discovery path and the loomcycle hash agent CLI computed content sha256 WITHOUT the body — three producers, three definitions of content. Plus mergeAgentDef dropped override.Code on the .md+yaml merge path. The fix is the conceptual one: a content-addressed hash has ONE definition; every producer converges on it. v0.20.0 lights up the Web UI PR 351 — Library renders code body as a monospace block alongside system prompt; create/fork modal grows a code-body textarea shown only when provider==code-js; validateLocal requires a body for code-js agents; the false "unreachable/handshake failed" message on MCP servers with empty discovered tools is reframed to admit lazy registration is normal . PR 352 closes a sibling MCPServerDef asymmetry on the same lesson — discovery now runs at ingestion create + fork run tools/list and fold the result into the same version — no v2, no separate manual rediscover step . Best-effort against unreachable peers bounded by the existing 30s budget , promote-gated, size-guarded metadata-only if discovered tools would push past MaxDefinitionBytes , opt-out with discover:false. discovered tools is not part of content sha256 so dedup is unaffected. PR 353 bumps @loomcycle/client to 0.20.0: ensureMcpServer reads discoveredToolCount straight from the create response — no separate rediscover round-trip on first registration; rediscover becomes an explicit force-refresh escape hatch. Same engineering lesson as yesterday's MCPServerDef static-vs-dynamic asymmetry-class post: every substrate primitive has both a yaml-loaded and a dynamically-created path; every seam must work the same on both; whichever path is the less-trafficked one will silently rot.