---
title: "MGS1033: a cacheable target composes an op that reaches outside the tree"
description: Fires when a cacheable target composes a spell op declaring reads-external or mutates-external. A replay then reports a verdict that has expired, or a side effect that never happened, and both look green.
tags: [MGS1033, magusfile, cache, skip_cache, scanners, observations, external]
---

# 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:

```text
[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](MGS1026.md) draws for a credential.

## Resolve it

For `mutates-external`, one answer. Declare the target uncacheable, with the reason:

```buzz
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:

```buzz
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](../../../concepts/cache.md) for the input class it joins, and
[Charms](../../../concepts/charms.md) 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](MGS1004.md)). 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.

## See also

- [Cache](../../../concepts/cache.md) - stated and probed observations, one input class
- [Charms](../../../concepts/charms.md) - the `update` charm, and why it is not `rw`
- [MGS1026](MGS1026.md) - the sibling check for a cacheable target reading a credential
