Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneeszack

Relationships

#3295 data.latest("otherns:model", name) returns the caller's own record when a local model has that name

Opened by zack · 10/12/2026· Shipped 10/12/2026

Severity: medium — an expression that explicitly asks for another namespace's record silently receives the caller's own same-named record and the method runs with it (exit 0, no warning), so automation acts on the wrong data. It needs a local model with the same name and a definition that is evaluated in the full expression context; no data is written or lost.

Affects: swamp 20261009.215038.0-sha.986f9022; the UAT test below still fails the same way on 20261012.141205.0-sha.58191ab3. At the time of writing, source HEAD dbd7ba9a still has it (src/domain/expressions/model_resolver.ts is unchanged between the two; the same line is present).

Summary

data.latest("<ns>:<model>", "<name>") is documented to read the named namespace only. When the calling repo has a model definition called <model> and the expression is evaluated in the full expression context, the namespace prefix is ignored: swamp looks up the caller's local model by type and id on the filesystem and returns its record. This happens for any prefix, including a namespace that does not exist (nosuchns:unique-model).

The same call is answered correctly ("found no data record") when the expression is evaluated in the lightweight context, so the result depends on something unrelated to the data: whether the definition's expressions match the "needs the model namespace" pattern. A model name that ends in -model inside the string literal is enough to match it, which is how the reproduction below triggers it; a definition that also reads model.* or file.* anywhere would do the same.

Steps to reproduce

d=$(mktemp -d) && cd "$d"
swamp init
swamp datastore namespace set infra

mkdir -p extensions/models/test
cat > extensions/models/test/producer.ts <<'EOF'
import { z } from "npm:zod@4";
export const model = {
  type: "@test/producer",
  version: "2026.01.01.1",
  globalArguments: z.object({}),
  resources: {
    payload: {
      description: "payload",
      schema: z.object({ owner: z.string() }),
      lifetime: "infinite" as const,
      garbageCollection: 10,
    },
  },
  methods: {
    produce: {
      description: "write one record",
      arguments: z.object({}),
      execute: async (_args: unknown, context: any) => {
        const h = await context.writeResource("payload", "item", { owner: "infra" });
        return { dataHandles: [h] };
      },
    },
  },
};
EOF

# Two local models in namespace "infra", each with one record called "item".
for m in unique-model alpha; do
  swamp model create @test/producer $m
  swamp model method run $m produce
done

# Nothing exists in namespace "security".
swamp data query 'ns == "security"' --json     # "total": 0

reader() {  # reader <model-name> <ns:model>
  swamp model create command/shell "$1"
  f=models/command/shell/$1.yaml
  grep -v '^methods:' "$f" > "$f.tmp"
  cat >> "$f.tmp" <<EOF
methods:
  execute:
    arguments:
      run: 'echo owner=\${{ data.latest("$2", "item").attributes.owner }}'
EOF
  mv "$f.tmp" "$f"
  swamp model method run "$1" execute; echo "exit=$?"
}

reader r1 security:unique-model    # BUG: prints owner=infra, exit=0
reader r2 nosuchns:unique-model    # BUG: prints owner=infra, exit=0
reader r3 security:alpha           # correct: exit=1, "found no data record"

Expected behaviour

r1 and r2 fail the same way r3 does: exit 1 with

data.latest("security:unique-model", "item") found no data record

because namespace security (and nosuchns) holds no such record.

What establishes it:

  • design/enablers/data-query.md lines 188-199, "Cross-namespace queries": data.latest("model", "name") is "Own namespace only (default)"; data.latest("infra:model", "name") is "Target namespace infra".
  • design/enablers/data-query.md lines 146-147: every helper adds && ns == "<own namespace>" "unless the model name has a namespace prefix (routeNamespace)".
  • design/enablers/expressions.md around lines 304-309: for a name with no namespace prefix the helpers also read the definition's data by type and id, but "Namespaced (ns:) and wildcard (*:) names match the tag only" — i.e. a prefixed name is not resolved through the local definition's type and id.
  • src/domain/expressions/model_resolver.ts doc comment on routeNamespace (lines 117-125): "ns:model" → target namespace, namespace == "ns".
  • swamp's own behaviour for security:alpha in the same repo, which has exactly the same data shape and correctly finds nothing.

Actual behaviour

$ swamp data query 'ns == "security"' --json
{ "predicate": "ns == \"security\"", "results": [], "total": 0, "limited": false }

$ swamp model method run r1 execute        # data.latest("security:unique-model", "item")
              owner=infra
    │    command      echo owner=infra          │
    │    stdout       owner=infra               │
exit=0

$ swamp model method run r2 execute        # data.latest("nosuchns:unique-model", "item")
              owner=infra
exit=0

$ swamp model method run r3 execute        # data.latest("security:alpha", "item")
Error: Expression in methods.execute.arguments.run could not be evaluated: Invalid expression: No such key: attributes
data.latest("security:alpha", "item") found no data record — check the model and data names, or use .?attributes if the record may not exist yet.
exit=1

No warning is logged in the failing cases. The record returned is the one swamp data query 'name == "item"' reports with "namespace": "infra".

Not reproduced, but following from the code path below: in a shared datastore where security really does hold a unique-model/item record, a caller in infra with its own unique-model would get its own record instead of security's, because the local lookup returns before the catalog is consulted.

Root cause

src/domain/expressions/model_resolver.ts at 986f9022, the latest accessor (starts line 1279):

if (!ns.isWildcard) {
  const coords =
    parseNamespacedModelName(rawModelName).namespace === undefined
      ? await resolveIdentities(ns.modelName)
      : coordsMap.get(ns.modelName);          // line 1290
  if (coords && coords.length > 0) {
    for (const { modelType, modelId } of coords) {
      ...
      const data = await this.dataRepo.findByName(modelType, modelId, dataName);
      if (data) { ... return record; }

For a namespace-qualified name it takes the coordinates of the caller's local definition with that bare name (coordsMap is built from the repo's own definitions, lines 751-760) and returns dataRepo.findByName(...) for them. ns.namespacePredicate is never applied on this path; the target namespace is only used by the catalog fallback further down (dataQueryService.getLatestRecord(ns.modelName, dataName, targetNs, ...)), which is reached only when the local lookup finds nothing.

coordsMap is populated only in the full expression context. The lightweight context passes an empty map, which is why security:alpha is correct. The full context is selected by requiresModelNamespace in src/domain/expressions/dependency_extractor.ts (MODEL_NAMESPACE_PATTERN = /(?<![.\w])(?:model|file)\b/); the -model in "security:unique-model" appears to satisfy it (inferred from the pattern and the observed difference between unique-model and alpha, not traced step by step).

The filesystem-first lookup was introduced by 2d793440 "perf(expressions): resolve data.latest() via direct filesystem lookup (swamp-club#918) (#1731)", a performance change. data.version goes through the catalog predicate with the namespace clause and was not seen to be affected.

How the UAT found it

Test file tests/cli/data/namespace/query_test.ts, test "swamp data query namespace-qualified latest does not fall back to the caller's namespace".

The test did not exist before the audit; it was added in swamp-uat commit d39ea54. The earlier namespace tests in that file exercised a prefixed data.latest only for a namespace that really held the record and from positions where the right record was the only candidate, so "the prefix is ignored and the local record is returned" and "the prefix is honoured" produced the same output. Nothing asked for a namespace that has no record while the caller owns a model of the same name.

The new test sets the repo namespace to infra, seeds unique-model there, asserts swamp data query 'ns == "security"' has total 0, checks that infra:unique-model reads index=1, and then requires data.latest("security:unique-model", "item-000001") to fail with exit 1 and the "found no data record" message. Against the installed swamp it fails with: Command "swamp model method run foreign-reader execute" was expected to fail but exited 0 (stdout index=1).

Run it from the swamp-uat repo:

deno test --allow-read --allow-write --allow-env --allow-run --allow-net \
  tests/cli/data/namespace/query_test.ts --filter 'namespace-qualified latest'
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 7 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/12/2026, 4:09:53 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
zack assigned zack10/12/2026, 3:15:54 PM

Sign in to post a ripple.