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.
The Hidden Danger in Your n8n RAG Pipeline: What Happens When You Send Internal Docs to ChatGPT?