MGS1033: a cacheable target composes an op that reaches outside the tree
A target composes a spell op that declares a relation to the world outside this tree, and is still cacheable:
[fail] cacheable external ops: 1 cacheable target(s) compose an op whose inputs or
effects the cache key cannot see, so a replay reports a verdict that has expired or a
side effect that never happened
.: target "image-scan" composes docker::trivy-image (magusfile.buzz): the op's
verdict comes from data outside this tree that no probe identifies; declare
skip_cache with a reason, or declare an observe probe on tool trivy in the spell so
the data's identity keys the cache
The two shapes
An op declares one of two values, and which one you have decides the fix.
mutates-external is an effect that lands somewhere else: a registry push, a
signature, a deploy. A replay does not perform it. The target reports success and
the artifact is not there, which surfaces far downstream in whatever assumed it was.
reads-external is a verdict that comes from a live feed: a vulnerability database,
an advisory index, a signature checked against a transparency log. A replay reports
what the feed said when the target last ran. The tree is unchanged, so the key is
unchanged, so the answer keeps being green after it stopped being true. That is the
whole reason this is a check rather than something you would notice: a stale scan
looks exactly like a clean one.
Why the target and not the op decides
magus does not make a target uncacheable on an op's say-so. An op knows what it reaches; only the target composing it knows whether that reach determines the result. A target that scans an image is a different case from one that merely lists what a registry holds before doing something local with it.
So the op declares the fact and the target answers for it - the same division MGS1026 draws for a credential.
Resolve it
For mutates-external, one answer. Declare the target uncacheable, with the reason:
magus\project({
"targets": {
"image-push": {"skip_cache": "pushes a registry digest per invocation; a replay would publish nothing"},
},
})
For reads-external, skip_cache works too, and it is usually the worse trade: it
forfeits caching forever to avoid a staleness that only matters when the feed actually
moved. The better answer is to put the feed's identity IN the key, so the target stays
cacheable and a hit can only replay a run made against the same data. That is a
declaration on the spell, beside the tool's version probe:
export fun mgs_getTools() > {str: Tool} {
return {
"trivy": Tool{
probe = Command{bin = "trivy", args = ["version"]},
key = VersionKey{upTo = VersionComponent.patch},
observe = Command{bin = "trivy", args = ["version", "--format", "json"]},
},
};
}
The probe's output lands in the key as an obs: line. See
Cache for the input class it joins, and
Charms for the update charm that refreshes the data
on purpose rather than by accident.
An observation probe only pays for itself when the tool can also run OFFLINE. A scanner that silently refreshes its own database on every run changes its verdict under an unchanged key, and observing it then records the copy it held a moment before it replaced it.
When to ignore it
Reaching the network is not the same as being external, and the check only sees what
an op declares. go mod tidy contacts a module proxy and declares nothing, because
go.sum pins what comes back: with the tree unchanged the answer is unchanged, and a
new import is a tree change. If an op in your own spell is marked and should not be,
the fix is the spell's declaration rather than a skip_cache on every target that
composes it.
What this check cannot see
It reads the target body statically, so it finds ops the walk can attribute to a
target - the same list magus describe target prints under spells. An op reached
through a helper the walk cannot follow is invisible to it, like an unreached
ctx.readsFiles (MGS1004). It under-reports rather than over-reports.
It also cannot see a raw proc\exec of the same binary. That is a real gap and it
argues for the op: a spell op declares what the raw exec only does.