Myco is a cognitive substrate that agents read + write on behalf of a human operator. That makes three attack surfaces load-bearing:
- Ingestion (
myco eat --path,--url): content an agent points at is absorbed into the substrate's raw notes. Hostile content could try to plant prompt-injection strings, credential-bearing files, or pathological inputs. - Canon + write-surface: every agent-writable path is declared
in
_canon.yaml::system.write_surface.allowed. A kernel that bypasses this declaration is a silent trust boundary break. - Extensibility seams:
.myco/plugins/(per-substrate) andsrc/myco/symbionts/(per-host) are load-import points. A hostile plugin could smuggle in capabilities the kernel's doctrine forbids.
This policy describes how Myco defends each and how to report regressions.
Myco is on rapid 0.8.x cadence. Only the latest minor receives security fixes. Older minors get advisory notes in the changelog.
| Version | Supported |
|---|---|
| 0.8.x (latest) | ✅ |
| 0.7.x | |
| 0.6.x | ❌ (frozen) |
| ≤ 0.5.x | ❌ (pre-rewrite, frozen) |
Landed in v0.5.8 + v0.5.9 + v0.5.10. All enforced without human-in-loop in the happy path:
- Write-surface discipline (R6): eight substrate-mutating verbs
(
eat,sporulate,digest,assimilate,fruit,ramify,molt,boot_brief) callcheck_write_allowedbefore any write; paths outside the declared surface raiseWriteSurfaceViolation(exit 3). Override viaMYCO_ALLOW_UNSAFE_WRITE=1is explicit + auditable. - Credential-file denylist:
.env*,id_rsa*,*.pem,*.key,.npmrc,.netrc,.pypirc,credentials*,secrets*,*.p12,*.pfxare refused by the text adapter regardless of extension. - Adapter size caps: 10 MB cap on every ingestion adapter
(
text_file,pdf_reader,html_reader,tabular,url_fetcher). A malicious 5 GB file cannot OOM the process. - SSRF guard in
url_fetcher: scheme allowlist (http/https only; nofile://,gopher://,data:); hostname DNS-resolved and rejected if it maps to loopback / link-local / private / multicast / reserved IPs. Redirect targets re-validated at every hop. - HTTP response byte-cap: streaming fetch aborts at 10 MB,
even if
Content-Lengthwas missing / lied. No single URL response can exceed the cap. - YAML-injection safety:
eat._render_noteusesyaml.safe_dump; every substrate-derived scalar (tags, source) passestrust.safe_frontmatter_fieldfirst (strips control chars, flattens newlines). - MCP pulse sanitisation:
substrate_idandcontract_versionflow throughsafe_frontmatter_fieldbefore entering any agent's context (strips ANSI escapes, caps length). - Atomic writes: every kernel write uses
atomic_utf8_write(temp file +os.fsync+os.replace). Concurrent readers never see a torn file; crash-between-write can't leave a half-written canon. - Bounded reads: every substrate read (canon / notes / graph
/ plugin source) routes through
bounded_read_text(10 MB default cap). Oversized reads raiseMycoErrorpre-read. - TOCTOU-safe note creation:
eat.append_noteusesos.open(..., O_WRONLY | O_CREAT | O_EXCL)in a collision-retry loop. Concurrenteatcalls with identical timestamps cannot overwrite each other. - Windows reserved-name guard:
germinaterejectssubstrate_id/entry_pointnames that match Windows reserved device names (CON/PRN/AUX/NUL/COM1-9/LPT1-9). Writing to these paths silently black-holes content on Windows; pre-v0.5.8 this was a silent loss path. - MP1 + MP2 + MP3 LLM-boundary enforcement (mechanical lint
refuses kernel (MP1), per-substrate plugin (MP2), and plugin
bytecode (MP3) imports of 31 known LLM provider SDKs). At v0.6.0
this generalised to a 3-state enum:
canon.system.llm_policy ∈ {"forbidden", "opt-in", "providers-declared"}. Defaultforbiddenkeeps L0 P1 strict; opt-in requires a contract-bumpingmolt(governance visible). - CL1 + CL2 + CL3 MCP-credential discipline (v0.6.0+). CL1
refuses to advertise MCP
samplingcapability whenllm_policy == "forbidden"; CL2 refuses to start the streamable- http server unlessmcp_auth.pyimports_redact_in_logs(regex-redactsBearer / access_token / refresh_tokenfrom log output); CL3 verifies_clear_token_after_call()zero-out runs after every sampling round-trip. - OAuth 2.1 streamable-http transport (v0.6.0+, optional). PKCE-
S256 enforced, RFC 8707 resource indicators required, JWKS
rotation supported,
audclaim validated, refresh-token rotation with 30s grace window. Usespython-jose(avoids the PyJWT algorithm-confusion attack class). Default transport remains stdio; OAuth only kicks in when the operator opts into network exposure. - Supply-chain hardening (v0.6.12+). CodeQL static analysis on
every push + PR + weekly cron (
security-and-qualityquery pack). OpenSSF Scorecard weekly + on default-branch push. SARIF results upload to GitHub Code scanning + the public Scorecard registry. Dependabot tracks bothpipandgithub-actionsecosystems with weekly batch updates. CODEOWNERS routes every PR to the maintainer.
Explicit non-goals — documented so integrators don't rely on Myco for checks it doesn't perform:
- Arbitrary code execution inside plugins: if a substrate
author installs a
.myco/plugins/module, that module runs with the kernel's full privileges. MF2 checks shape, not semantics; MP2 checks provider imports, not arbitrary behaviour. Vet your plugins. - Cross-substrate trust:
propagatewrites into a downstream substrate'snotes/raw/. It does not verify the downstream substrate's authenticity; if you push to a malicious dst, you leak the pushed notes. Validate destinations. - Secret redaction at ingest: the credential-file denylist
stops obvious cases (
.env,id_rsa), but a hostile string containingOPENAI_API_KEY=sk-...inside a regular Markdown file IS absorbed verbatim. Use pre-commit hooks / secret scanners upstream ofmyco eat. - Full sandboxing of agent-driven writes: R6 prevents writes outside the declared surface; it does not prevent the agent from deleting or corrupting content inside the surface. Version-control your substrate (git) as the last line of defence.
Preferred: open a GitHub Security Advisory (the Security tab's "Report a vulnerability" button). This keeps the report private until a fix ships.
Fallback (if you can't use the advisory flow): email the
maintainer address listed in the repo's pyproject.toml
[project].authors block.
Do NOT open a public issue for a vulnerability. A public issue before a fix ships lets every substrate-on-network be exploited before upgrade is possible.
- Acknowledgement: within 7 days.
- Triage verdict (accept / decline / need-more-info): within 14 days.
- Fix release cadence:
- Critical (active exploitation, kernel-write bypass, substrate-wipe vector) → patch release within 7 days of triage.
- High (data-leak, DoS, boundary violation) → patch release in the next minor (≤ 21 days).
- Medium / low → next minor release on normal cadence.
Every disclosed + fixed vulnerability is credited by name (or
pseudonym of the reporter's choice) in docs/contract_changelog.md
for the version that lands the fix, and in the corresponding
GitHub Security Advisory. If the reporter prefers anonymity,
we respect that.
- Every security-related doctrine change passes through the
same
fruit(3-round craft) +molt(contract bump) loop as any other contract edit. No silent tightening of the security boundary. docs/primordia/v0_5_8_discipline_enforcement_craft_2026-04-21.mddocumented the R6 mechanical-enforcement landing + the credential/SSRF/size-cap hardening. Future security crafts live underdocs/primordia/with the same shape.