Summary
Make moat.yaml configs composable and shareable: let a config inherit from one or more base configs (extends), and let those bases be resolved from a shared location (local path, git ref, or URL) so a team can publish a vetted base policy once and reuse it across many projects.
Today every moat.yaml is standalone. Teams running many agents end up copy-pasting the same grants, network policy, and dependency settings into every repo — which drifts and is hard to keep consistent. This adds inheritance and sharing so there's a single source of truth.
Scope note: this issue is config composition + sharing only. Signing / attestation of shared configs is explicitly out of scope here and can be layered on later.
Motivation
- Eliminate duplication. A standard "python-agent" or "locked-down" policy (grants, network allowlist, sensitive-path denies, dependencies) gets defined once and inherited, not re-pasted per repo.
- Consistency + central updates. Tighten the team base policy in one place and every project that extends it picks up the change.
- Lower the barrier to a good default. New projects start from a vetted base instead of a blank
moat.yaml.
Proposed mechanism
Add an extends key that lists one or more base configs, resolved and deep-merged under the local config:
# moat.yaml
extends:
- ./base/locked-down.yaml # local path
- github:myorg/moat-configs//python-agent.yaml@v1 # shared, pinned to a ref
grants:
- github # merges with / overrides the bases
network:
allow_hosts:
- api.internal.example.com # appended to base allow_hosts
Resolution sources (first cut)
- Local/relative path —
./base.yaml, ../shared/base.yaml
- Git ref —
github:org/repo//path/to/config.yaml@ref (pin to a tag/sha; document that an unpinned ref is resolved at run time and is not reproducible)
- (Optional, later) plain HTTPS URL
Merge semantics (needs to be precise and documented)
- Bases are applied in order; the local file is applied last and wins on conflict.
- Scalars: later value overrides earlier.
- Maps: deep-merged key by key.
- Lists: define one explicit default and document it — proposal: append/union with de-dup for additive things (
grants, network.allow_hosts), since that's the common intent. Provide an escape hatch to fully replace a list rather than append (e.g. a !replace tag or a lists: replace directive) so a child can intentionally drop an inherited entry.
- Removal: support explicitly removing an inherited entry (e.g. a
network.deny_hosts-style negation or a documented "remove this key" sentinel), so a child can opt out of something a base granted.
These rules are the part most likely to bite — see Codebase Invariants on companion-case testing. Every merge rule needs a test for both directions (value present in base only / child only / both; list append vs. replace; removal).
Configuration / CLI sketch
moat config resolve # print the fully-merged, flattened config for this dir
moat config resolve --explain # show which base each field came from
A resolve --explain view matters: once configs inherit, "why is this host allowed?" needs a traceable answer.
Scope for a first cut
Edge cases / guardrails
- Cycles — detect and error clearly rather than looping.
- Missing/unreachable base — fail with an actionable message (which base, where it was referenced); don't silently skip it.
- Reproducibility — an unpinned git ref or URL means the effective config can change out from under a run. Document this; consider warning when a base is unpinned.
- Caching — remote bases should be cached locally; define cache location and a refresh/clear path.
- Empty / malformed base — handle empty strings, trailing-separator names, and malformed YAML the same way the standalone loader does.
Open questions
- Single
extends chain vs. a list of multiple bases — start with a list (more flexible) or a single base (simpler merge story)?
- Default list-merge behavior: append-union vs. replace — which is least surprising as the default? (Proposal above: append-union, with explicit replace opt-out.)
- Where do remote bases cache, and how are they pinned/locked (a
moat.lock-style record of resolved shas for reproducibility)?
Out of scope (tracked separately)
- Signing / attestation of shared configs — deliberately deferred. This issue gets sharing + inheritance working first; provenance/verification of bases is a later layer.
- A hosted registry —
extends from a git repo or path covers the sharing need without standing up registry infrastructure.
Summary
Make
moat.yamlconfigs composable and shareable: let a config inherit from one or more base configs (extends), and let those bases be resolved from a shared location (local path, git ref, or URL) so a team can publish a vetted base policy once and reuse it across many projects.Today every
moat.yamlis standalone. Teams running many agents end up copy-pasting the same grants, network policy, and dependency settings into every repo — which drifts and is hard to keep consistent. This adds inheritance and sharing so there's a single source of truth.Scope note: this issue is config composition + sharing only. Signing / attestation of shared configs is explicitly out of scope here and can be layered on later.
Motivation
moat.yaml.Proposed mechanism
Add an
extendskey that lists one or more base configs, resolved and deep-merged under the local config:Resolution sources (first cut)
./base.yaml,../shared/base.yamlgithub:org/repo//path/to/config.yaml@ref(pin to a tag/sha; document that an unpinned ref is resolved at run time and is not reproducible)Merge semantics (needs to be precise and documented)
grants,network.allow_hosts), since that's the common intent. Provide an escape hatch to fully replace a list rather than append (e.g. a!replacetag or alists: replacedirective) so a child can intentionally drop an inherited entry.network.deny_hosts-style negation or a documented "remove this key" sentinel), so a child can opt out of something a base granted.These rules are the part most likely to bite — see Codebase Invariants on companion-case testing. Every merge rule needs a test for both directions (value present in base only / child only / both; list append vs. replace; removal).
Configuration / CLI sketch
A
resolve --explainview matters: once configs inherit, "why is this host allowed?" needs a traceable answer.Scope for a first cut
extendskey accepting a list of basesmoat config resolve/--explainextends, resolution sources, merge semantics, reproducibility caveatsEdge cases / guardrails
Open questions
extendschain vs. a list of multiple bases — start with a list (more flexible) or a single base (simpler merge story)?moat.lock-style record of resolved shas for reproducibility)?Out of scope (tracked separately)
extendsfrom a git repo or path covers the sharing need without standing up registry infrastructure.