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 recordbecause namespace security (and nosuchns) holds no such record.
What establishes it:
design/enablers/data-query.mdlines 188-199, "Cross-namespace queries":data.latest("model", "name")is "Own namespace only (default)";data.latest("infra:model", "name")is "Target namespaceinfra".design/enablers/data-query.mdlines 146-147: every helper adds&& ns == "<own namespace>""unless the model name has a namespace prefix (routeNamespace)".design/enablers/expressions.mdaround 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.tsdoc comment onrouteNamespace(lines 117-125):"ns:model"→ target namespace,namespace == "ns".- swamp's own behaviour for
security:alphain 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=1No 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'Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.