From 1f183b8946fd3ca24c271252b4db26ce6d034799 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 9 Jul 2026 17:58:47 +0200 Subject: [PATCH 1/3] Serve index-enriched file-meta for cross-realm file links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stored relationships for linked files can carry only links.self (no data.type), so loadLinks falls back to fieldExpectsFileMeta, which infers file-ness from whether files of that type exist in the card's own realm — wrong for a cross-realm file link when the card's realm holds no such files. The cross-realm fetch then used the card mime, and a card-mime request for a file returned a minimal document built from the raw file (base attributes only), dropping index-derived fields like `kind`. That partial resource lands in the card document's included bucket, the host caches it, and markdown skills configured on a system card silently fail to attach (kind is unset, so the file no longer counts as a skill). Serve file-meta through one path that prefers the index-enriched document and falls back to the raw-file form only when the file isn't indexed — used by both the file-meta route and card-mime requests that turn out to target files. The response keeps the mime the route was matched on; callers discriminate the payload via data.type. fetchCrossRealmLinks now also asks for the file-meta mime outright when the relationship is known to expect a file. Co-Authored-By: Claude Fable 5 --- .../realm-server/tests/card-endpoints-test.ts | 41 ++++++++++++++- .../realm-index-query-engine.ts | 32 +++++++++--- packages/runtime-common/realm.ts | 50 +++++++++++++------ 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/packages/realm-server/tests/card-endpoints-test.ts b/packages/realm-server/tests/card-endpoints-test.ts index bd62b5ad22..999540f7a7 100644 --- a/packages/realm-server/tests/card-endpoints-test.ts +++ b/packages/realm-server/tests/card-endpoints-test.ts @@ -4197,6 +4197,7 @@ module(basename(import.meta.filename), function () { // surfacing as an HTTP 500. const providerRealmURL = 'http://127.0.0.1:5531/test/'; const consumerRealmURL = 'http://127.0.0.1:5532/test/'; + let providerRequest: RealmRequest; let consumerRequest: RealmRequest; setupPermissionedRealmsCached(hooks, { @@ -4208,7 +4209,11 @@ module(basename(import.meta.filename), function () { '@node-test_realm:localhost': ['read', 'realm-owner'], }, fileSystem: { - 'instructions.md': '# Cross-realm instructions', + 'instructions.md': `--- +boxel: + kind: skill +--- +# Cross-realm instructions`, }, }, { @@ -4253,6 +4258,10 @@ module(basename(import.meta.filename), function () { ], onRealmSetup({ realms }) { let latestRealms = realms.slice(-2); + providerRequest = withRealmPath( + supertest(latestRealms[0].realmHttpServer), + new URL(providerRealmURL), + ); consumerRequest = withRealmPath( supertest(latestRealms[1].realmHttpServer), new URL(consumerRealmURL), @@ -4298,6 +4307,36 @@ module(basename(import.meta.filename), function () { 'cross-realm linked resource is a file-meta resource', ); assert.strictEqual(linkedFile?.attributes?.name, 'instructions.md'); + // Index-derived attributes must survive the cross-realm hop: a partial + // file-meta here gets cached by the host, where a markdown skill + // without `kind: 'skill'` silently stops counting as a skill. + assert.strictEqual( + linkedFile?.attributes?.kind, + 'skill', + 'cross-realm file-meta carries index-derived attributes', + ); + }); + + test('serves index-enriched file-meta for a card-mime request on a file URL', async function (assert) { + let response = await providerRequest + .get('/instructions.md') + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual( + response.status, + 200, + `HTTP 200 status: ${response.text}`, + ); + assert.strictEqual( + response.body.data?.type, + 'file-meta', + 'a card-mime request for a file path returns a file-meta document', + ); + assert.strictEqual( + response.body.data?.attributes?.kind, + 'skill', + 'the document is index-enriched, not built from the raw file alone', + ); }); }); diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index b9f4c6a744..d0e1398c8a 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -1372,14 +1372,23 @@ export class RealmIndexQueryEngine { urls: string[], invocationId: string, layerIndex: number, - linkContext?: Map, + linkContext?: Map, ): Promise | FileMetaResource>> { let entries = await Promise.all( urls.map(async (url) => { let response: Response; + // A file link is requested with the file-meta mime so the serving + // realm returns the index-enriched document (consumers rely on + // index-derived attributes like a markdown skill's `kind`). When the + // link type isn't known, fall back to the card mime — the serving + // realm answers a card-mime request for a file path with a file-meta + // document, enriched the same way. + let accept = linkContext?.get(url)?.expectsFileMeta + ? SupportedMimeType.FileMeta + : SupportedMimeType.CardJson; try { response = await this.#fetch(url, { - headers: { Accept: SupportedMimeType.CardJson }, + headers: { Accept: accept }, }); } catch (err: unknown) { let message = @@ -1602,9 +1611,13 @@ export class RealmIndexQueryEngine { let inRealmCardURLs = new Set(); let inRealmFileURLs = new Set(); let crossRealmURLs = new Set(); - // Maps each cross-realm URL to the field that produced it, so a - // non-card link can name the offending field in its error. - let crossRealmFieldNames = new Map(); + // Maps each cross-realm URL to the field that produced it — so a + // non-card link can name the offending field in its error, and so the + // fetch can request the file-meta representation for file links. + let crossRealmLinkContext = new Map< + string, + { fieldName: string; expectsFileMeta: boolean } + >(); for (let item of layer) { let { resource, applyLinkFields } = item; @@ -1714,8 +1727,11 @@ export class RealmIndexQueryEngine { } } else { crossRealmURLs.add(linkURL.href); - if (!crossRealmFieldNames.has(linkURL.href)) { - crossRealmFieldNames.set(linkURL.href, { fieldName }); + if (!crossRealmLinkContext.has(linkURL.href)) { + crossRealmLinkContext.set(linkURL.href, { + fieldName, + expectsFileMeta, + }); } } @@ -1764,7 +1780,7 @@ export class RealmIndexQueryEngine { [...crossRealmURLs], invocationId, currentLayerIndex, - crossRealmFieldNames, + crossRealmLinkContext, ) : Promise.resolve( new Map | FileMetaResource>(), diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index 96fa6facfe..a484fc08f1 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -4347,6 +4347,37 @@ export class Realm { return await this.#adapter.exists(localPath); } + // File-meta response for a file path, preferring the index-enriched + // document: consumers rely on index-derived attributes (e.g. a markdown + // skill only counts as a skill when its file-meta carries `kind: + // 'skill'`). Falls back to the raw-file form when the file isn't indexed. + // Serves both the file-meta route and card-mime requests that turn out to + // target files, such as cross-realm file links fetched by loadLinks; the + // response is labeled with the mime the route was matched on, and callers + // discriminate the payload via `data.type`. + private async fileMetaResponse( + requestContext: RequestContext, + localPath: LocalPath, + responseContentType: SupportedMimeType, + ): Promise { + let fileEntry = await this.#realmIndexQueryEngine.file( + this.paths.fileURL(localPath), + ); + if (fileEntry) { + return await this.fileMetaDocumentFromIndex( + requestContext, + localPath, + fileEntry, + responseContentType, + ); + } + return await this.fileMetaDocument( + requestContext, + localPath, + responseContentType, + ); + } + private async fileMetaDocument( requestContext: RequestContext, localPath: LocalPath, @@ -4410,6 +4441,7 @@ export class Realm { requestContext: RequestContext, localPath: LocalPath, fileEntry: IndexedFile, + responseContentType: SupportedMimeType = SupportedMimeType.FileMeta, ): Promise { let fileURL = this.paths.fileURL(localPath).href; let name = localPath.split('/').pop() ?? localPath; @@ -4495,7 +4527,7 @@ export class Realm { body: JSON.stringify(doc, null, 2), init: { headers: { - 'content-type': SupportedMimeType.FileMeta, + 'content-type': responseContentType, }, }, requestContext, @@ -4510,17 +4542,7 @@ export class Realm { if (localPath === '') { localPath = 'index'; } - let fileEntry = await this.#realmIndexQueryEngine.file( - this.paths.fileURL(localPath), - ); - if (fileEntry) { - return await this.fileMetaDocumentFromIndex( - requestContext, - localPath, - fileEntry, - ); - } - let fileResponse = await this.fileMetaDocument( + let fileResponse = await this.fileMetaResponse( requestContext, localPath, SupportedMimeType.FileMeta, @@ -5160,7 +5182,7 @@ export class Realm { // document so the caller receives valid JSON it can // discriminate via `data.type === 'file-meta'` — instead of // raw binary bytes that crash a downstream `response.json()`. - let fileMeta = await this.fileMetaDocument( + let fileMeta = await this.fileMetaResponse( requestContext, localPath, SupportedMimeType.CardJson, @@ -5210,7 +5232,7 @@ export class Realm { }); if (maybeError === undefined) { if (await this.nonJsonFileExists(localPath)) { - let fileMeta = await this.fileMetaDocument( + let fileMeta = await this.fileMetaResponse( requestContext, localPath, SupportedMimeType.CardJson, From bf035703eb4ad2ea6138988b32056232e90995ec Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Fri, 10 Jul 2026 10:37:31 +0200 Subject: [PATCH 2/3] Detect file links by URL extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Whether a link targets a file or a card is knowable from the URL alone: card ids never carry a file extension, file URLs always do. Use that in loadLinks to classify relationships and to pick the accept mime for cross-realm link fetches, so a cross-realm file link is served the index-enriched file-meta document (a markdown skill only counts as a skill when its file-meta carries kind). This replaces fieldExpectsFileMeta, which consulted the field definition and then probed whether files of that type exist in the card's own realm — wrong for a cross-realm link when the card's realm holds no such files. The serving-side changes from the previous commit are reverted: with the requester asking correctly, they are no longer needed for this fix. Co-Authored-By: Claude Fable 5 --- .../realm-server/tests/card-endpoints-test.ts | 27 ---- packages/runtime-common/file-def-code-ref.ts | 18 ++- .../realm-index-query-engine.ts | 139 ++++-------------- packages/runtime-common/realm.ts | 64 +++----- 4 files changed, 63 insertions(+), 185 deletions(-) diff --git a/packages/realm-server/tests/card-endpoints-test.ts b/packages/realm-server/tests/card-endpoints-test.ts index 999540f7a7..ec48dcb12e 100644 --- a/packages/realm-server/tests/card-endpoints-test.ts +++ b/packages/realm-server/tests/card-endpoints-test.ts @@ -4197,7 +4197,6 @@ module(basename(import.meta.filename), function () { // surfacing as an HTTP 500. const providerRealmURL = 'http://127.0.0.1:5531/test/'; const consumerRealmURL = 'http://127.0.0.1:5532/test/'; - let providerRequest: RealmRequest; let consumerRequest: RealmRequest; setupPermissionedRealmsCached(hooks, { @@ -4258,10 +4257,6 @@ boxel: ], onRealmSetup({ realms }) { let latestRealms = realms.slice(-2); - providerRequest = withRealmPath( - supertest(latestRealms[0].realmHttpServer), - new URL(providerRealmURL), - ); consumerRequest = withRealmPath( supertest(latestRealms[1].realmHttpServer), new URL(consumerRealmURL), @@ -4316,28 +4311,6 @@ boxel: 'cross-realm file-meta carries index-derived attributes', ); }); - - test('serves index-enriched file-meta for a card-mime request on a file URL', async function (assert) { - let response = await providerRequest - .get('/instructions.md') - .set('Accept', 'application/vnd.card+json'); - - assert.strictEqual( - response.status, - 200, - `HTTP 200 status: ${response.text}`, - ); - assert.strictEqual( - response.body.data?.type, - 'file-meta', - 'a card-mime request for a file path returns a file-meta document', - ); - assert.strictEqual( - response.body.data?.attributes?.kind, - 'skill', - 'the document is index-enriched, not built from the raw file alone', - ); - }); }); module('Query-backed relationships runtime resolver', function (hooks) { diff --git a/packages/runtime-common/file-def-code-ref.ts b/packages/runtime-common/file-def-code-ref.ts index feafe839ee..29d34671de 100644 --- a/packages/runtime-common/file-def-code-ref.ts +++ b/packages/runtime-common/file-def-code-ref.ts @@ -77,13 +77,25 @@ export function isFileDefCodeRef( return false; } +function extensionOf(url: URL): string { + let name = url.pathname.split('/').pop() ?? ''; + let dot = name.lastIndexOf('.'); + return dot <= 0 ? '' : name.slice(dot).toLowerCase(); +} + +// Whether a URL names a file rather than a card. Card ids never carry a +// file extension (an instance's `.json` is stripped from its id), so an +// extension on the last path segment is a reliable tell that the URL +// targets a file. +export function urlNamesFile(url: URL): boolean { + return extensionOf(url) !== ''; +} + export function resolveFileDefCodeRef( fileURL: URL, virtualNetwork: VirtualNetwork, ): ResolvedCodeRef { - let name = fileURL.pathname.split('/').pop() ?? ''; - let dot = name.lastIndexOf('.'); - let extension = dot === -1 ? '' : name.slice(dot).toLowerCase(); + let extension = extensionOf(fileURL); let mapping = extension ? FILEDEF_CODE_REF_BY_EXTENSION[extension] : undefined; diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index d0e1398c8a..89d8e56091 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -84,7 +84,8 @@ import { type SearchEntryFieldset, type SearchEntryQuery, } from './search-entry.ts'; -import { getImmediateFieldDef, type FieldDefinition } from './definitions.ts'; +import type { FieldDefinition } from './definitions.ts'; +import { urlNamesFile } from './file-def-code-ref.ts'; import { normalizeQueryDefinition, buildQuerySearchURL, @@ -697,71 +698,6 @@ export class RealmIndexQueryEngine { return fileMatch && !instanceMatch; } - // When a relationship in the pristine_doc is missing data.type (stale - // index data from before the fix that added data to NotLoadedValue - // serialization), we need to consult the field definition to determine - // whether the relationship targets a FileDef or a CardDef. - private async fieldExpectsFileMeta( - resource: LooseCardResource | FileMetaResource, - fieldKey: string, - opts?: Options, - ): Promise { - if (!resource.meta?.adoptsFrom) { - return false; - } - let relativeTo = resource.id - ? this.#realm.virtualNetwork.toURL(resource.id) - : this.realmURL; - let codeRef = codeRefWithAbsoluteIdentifier( - resource.meta.adoptsFrom, - relativeTo, - undefined, - this.#realm.virtualNetwork, - ); - if (!isResolvedCodeRef(codeRef)) { - return false; - } - try { - let definition: import('./definitions.ts').Definition | undefined; - if (opts?.cacheOnlyDefinitions) { - definition = - await this.#definitionLookup.lookupCachedDefinition(codeRef); - if (!definition) { - return false; - } - } else { - definition = await this.#definitionLookup.lookupDefinition(codeRef, { - ...(opts?.priority !== undefined ? { priority: opts.priority } : {}), - }); - } - if (!definition) { - return false; - } - // Strip the linksToMany index suffix (e.g., "friends.0" -> "friends") - let fieldName = fieldKey.includes('.') - ? fieldKey.slice(0, fieldKey.indexOf('.')) - : fieldKey; - let fieldDefinition = getImmediateFieldDef(definition, fieldName); - if (!fieldDefinition) { - return false; - } - let fieldCardRef = fieldDefinition.fieldOrCard; - let isFileType = await this.#indexQueryEngine.hasFileType( - this.realmURL, - fieldCardRef, - opts, - ); - let isInstanceType = await this.#indexQueryEngine.hasInstanceType( - this.realmURL, - fieldCardRef, - opts, - ); - return isFileType && !isInstanceType; - } catch { - return false; - } - } - async fetchCardTypeSummary() { let results = await this.#indexQueryEngine.fetchCardTypeSummary( new URL(this.#realm.url), @@ -1372,18 +1308,17 @@ export class RealmIndexQueryEngine { urls: string[], invocationId: string, layerIndex: number, - linkContext?: Map, + linkContext?: Map, ): Promise | FileMetaResource>> { let entries = await Promise.all( urls.map(async (url) => { let response: Response; - // A file link is requested with the file-meta mime so the serving - // realm returns the index-enriched document (consumers rely on - // index-derived attributes like a markdown skill's `kind`). When the - // link type isn't known, fall back to the card mime — the serving - // realm answers a card-mime request for a file path with a file-meta - // document, enriched the same way. - let accept = linkContext?.get(url)?.expectsFileMeta + // A file link (the URL carries a file extension; card ids never do) + // is requested with the file-meta mime so the serving realm returns + // the index-enriched document — consumers rely on index-derived + // attributes like a markdown skill's `kind`, which the card-mime + // fallback for file paths omits. + let accept = urlNamesFile(new URL(url)) ? SupportedMimeType.FileMeta : SupportedMimeType.CardJson; try { @@ -1611,13 +1546,9 @@ export class RealmIndexQueryEngine { let inRealmCardURLs = new Set(); let inRealmFileURLs = new Set(); let crossRealmURLs = new Set(); - // Maps each cross-realm URL to the field that produced it — so a - // non-card link can name the offending field in its error, and so the - // fetch can request the file-meta representation for file links. - let crossRealmLinkContext = new Map< - string, - { fieldName: string; expectsFileMeta: boolean } - >(); + // Maps each cross-realm URL to the field that produced it, so a + // non-card link can name the offending field in its error. + let crossRealmFieldNames = new Map(); for (let item of layer) { let { resource, applyLinkFields } = item; @@ -1667,35 +1598,24 @@ export class RealmIndexQueryEngine { } processed.add(key); - let relationshipType = relationship.data?.type as - | typeof CardResourceType - | typeof FileMetaResourceType - | undefined; - let expectsFileMeta = relationshipType === FileMetaResourceType; - let expectsCard = relationshipType === CardResourceType; - // Stale index payloads can incorrectly record file relationships - // as type "card" (or omit type entirely) when linked files were - // indexed after instances. Trust the field declaration in that - // case. - if ( - !expectsFileMeta && - (relationshipType === CardResourceType || !relationshipType) - ) { - expectsFileMeta = await this.fieldExpectsFileMeta( - resource, - key, - activeOpts, - ); - if (expectsFileMeta) { - expectsCard = false; - } - } - let vn = this.#realm.virtualNetwork; let linkURL = vn.resolveURL( relationship.links.self, resource.id ? vn.toURL(resource.id) : realmURL, ); + + let relationshipType = relationship.data?.type as + | typeof CardResourceType + | typeof FileMetaResourceType + | undefined; + // Card ids never carry a file extension, so the URL itself says + // whether the link targets a file. That also corrects stale index + // payloads which record file relationships as type "card" (or omit + // the type) when linked files were indexed after instances. + let expectsFileMeta = + relationshipType === FileMetaResourceType || urlNamesFile(linkURL); + let expectsCard = + relationshipType === CardResourceType && !expectsFileMeta; let resolvedSelf: string; try { resolvedSelf = vn.resolveURL( @@ -1727,11 +1647,8 @@ export class RealmIndexQueryEngine { } } else { crossRealmURLs.add(linkURL.href); - if (!crossRealmLinkContext.has(linkURL.href)) { - crossRealmLinkContext.set(linkURL.href, { - fieldName, - expectsFileMeta, - }); + if (!crossRealmFieldNames.has(linkURL.href)) { + crossRealmFieldNames.set(linkURL.href, { fieldName }); } } @@ -1780,7 +1697,7 @@ export class RealmIndexQueryEngine { [...crossRealmURLs], invocationId, currentLayerIndex, - crossRealmLinkContext, + crossRealmFieldNames, ) : Promise.resolve( new Map | FileMetaResource>(), diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index a484fc08f1..3f603a0e61 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -59,6 +59,7 @@ import { CardError, responseWithError, formattedError, + stringifyErrorForLog, unsupportedMediaType, type SerializedError, } from './error.ts'; @@ -171,7 +172,7 @@ import type { FileWatcherEventContent, RealmEventContent, UpdateRealmEventContent, -} from 'https://cardstack.com/base/matrix-event'; +} from '@cardstack/base/matrix-event'; import type { AtomicOperation, AtomicOperationResult, @@ -2128,11 +2129,12 @@ export class Realm { }, ); settled.catch((err: unknown) => { - let message = err instanceof Error ? err.message : String(err); // Covers worker job rejection AND post-worker realm-side work // (onInvalidation / handleExecutableInvalidations / broadcast). this.#log.error( - `Deferred indexing chain failed for ${this.url}: ${message}`, + `Deferred indexing chain failed for ${this.url} (urls: ${urls + .map((u) => u.href) + .join(', ')}): ${stringifyErrorForLog(err)}`, ); }); } @@ -2626,12 +2628,8 @@ export class Realm { ); }, (err: unknown) => { - let detail = - err instanceof Error - ? `${err.message}${err.stack ? `\n${err.stack}` : ''}` - : String(err); this.#log.error( - `Deferred delete-indexing chain failed for ${url.href} after ${Date.now() - enqueueStart}ms: ${detail}`, + `Deferred delete-indexing chain failed for ${url.href} after ${Date.now() - enqueueStart}ms: ${stringifyErrorForLog(err)}`, ); }, ); @@ -4347,37 +4345,6 @@ export class Realm { return await this.#adapter.exists(localPath); } - // File-meta response for a file path, preferring the index-enriched - // document: consumers rely on index-derived attributes (e.g. a markdown - // skill only counts as a skill when its file-meta carries `kind: - // 'skill'`). Falls back to the raw-file form when the file isn't indexed. - // Serves both the file-meta route and card-mime requests that turn out to - // target files, such as cross-realm file links fetched by loadLinks; the - // response is labeled with the mime the route was matched on, and callers - // discriminate the payload via `data.type`. - private async fileMetaResponse( - requestContext: RequestContext, - localPath: LocalPath, - responseContentType: SupportedMimeType, - ): Promise { - let fileEntry = await this.#realmIndexQueryEngine.file( - this.paths.fileURL(localPath), - ); - if (fileEntry) { - return await this.fileMetaDocumentFromIndex( - requestContext, - localPath, - fileEntry, - responseContentType, - ); - } - return await this.fileMetaDocument( - requestContext, - localPath, - responseContentType, - ); - } - private async fileMetaDocument( requestContext: RequestContext, localPath: LocalPath, @@ -4441,7 +4408,6 @@ export class Realm { requestContext: RequestContext, localPath: LocalPath, fileEntry: IndexedFile, - responseContentType: SupportedMimeType = SupportedMimeType.FileMeta, ): Promise { let fileURL = this.paths.fileURL(localPath).href; let name = localPath.split('/').pop() ?? localPath; @@ -4527,7 +4493,7 @@ export class Realm { body: JSON.stringify(doc, null, 2), init: { headers: { - 'content-type': responseContentType, + 'content-type': SupportedMimeType.FileMeta, }, }, requestContext, @@ -4542,7 +4508,17 @@ export class Realm { if (localPath === '') { localPath = 'index'; } - let fileResponse = await this.fileMetaResponse( + let fileEntry = await this.#realmIndexQueryEngine.file( + this.paths.fileURL(localPath), + ); + if (fileEntry) { + return await this.fileMetaDocumentFromIndex( + requestContext, + localPath, + fileEntry, + ); + } + let fileResponse = await this.fileMetaDocument( requestContext, localPath, SupportedMimeType.FileMeta, @@ -5182,7 +5158,7 @@ export class Realm { // document so the caller receives valid JSON it can // discriminate via `data.type === 'file-meta'` — instead of // raw binary bytes that crash a downstream `response.json()`. - let fileMeta = await this.fileMetaResponse( + let fileMeta = await this.fileMetaDocument( requestContext, localPath, SupportedMimeType.CardJson, @@ -5232,7 +5208,7 @@ export class Realm { }); if (maybeError === undefined) { if (await this.nonJsonFileExists(localPath)) { - let fileMeta = await this.fileMetaResponse( + let fileMeta = await this.fileMetaDocument( requestContext, localPath, SupportedMimeType.CardJson, From 57ede98d93f5955e0e570cd39e0a41aa75cd8a5e Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Fri, 10 Jul 2026 10:59:33 +0200 Subject: [PATCH 3/3] Detect file links only by known FileDef extensions Card ids can legitimately contain dots (e.g. a versioned ModelConfiguration instance like model-4.6), so treating any dotted path segment as a file would misclassify such cross-realm card links and 404 them against the file-meta route. Count only extensions registered in FILEDEF_CODE_REF_BY_EXTENSION as file tells, and cover the dotted-id card link with a regression test. Co-Authored-By: Claude Fable 5 --- .../realm-server/tests/card-endpoints-test.ts | 69 +++++++++++++++++++ packages/runtime-common/file-def-code-ref.ts | 12 ++-- .../realm-index-query-engine.ts | 19 ++--- 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/packages/realm-server/tests/card-endpoints-test.ts b/packages/realm-server/tests/card-endpoints-test.ts index ec48dcb12e..fb648ea401 100644 --- a/packages/realm-server/tests/card-endpoints-test.ts +++ b/packages/realm-server/tests/card-endpoints-test.ts @@ -4213,6 +4213,27 @@ boxel: kind: skill --- # Cross-realm instructions`, + 'model.gts': ` + import { CardDef, field, contains } from "https://cardstack.com/base/card-api"; + import StringField from "https://cardstack.com/base/string"; + + export class Model extends CardDef { + @field cardTitle = contains(StringField); + } + `, + 'model-4.6.json': { + data: { + attributes: { + cardTitle: 'Model 4.6', + }, + meta: { + adoptsFrom: { + module: rri('./model'), + name: 'Model', + }, + }, + }, + }, }, }, { @@ -4230,8 +4251,29 @@ boxel: export class SkillCard extends CardDef { @field cardTitle = contains(StringField); @field instructionsSource = linksTo(MarkdownDef); + @field model = linksTo(CardDef); } `, + 'skill-with-model.json': { + data: { + attributes: { + cardTitle: 'Skill with dotted model link', + }, + relationships: { + model: { + links: { + self: `${providerRealmURL}model-4.6`, + }, + }, + }, + meta: { + adoptsFrom: { + module: rri('./skill-card'), + name: 'SkillCard', + }, + }, + }, + }, 'skill.json': { data: { attributes: { @@ -4311,6 +4353,33 @@ boxel: 'cross-realm file-meta carries index-derived attributes', ); }); + + test('serves a card linking to a cross-realm card whose id contains a dot', async function (assert) { + // Card ids may legitimately contain dots (e.g. a versioned + // ModelConfiguration). The file-link detection keys on known FileDef + // extensions, so ".6" must not classify this link as a file. + let response = await consumerRequest + .get('/skill-with-model') + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual( + response.status, + 200, + `HTTP 200 status: ${response.text}`, + ); + + let doc = response.body as LooseSingleCardDocument; + let included = doc.included ?? []; + let linkedCard = included.find( + (resource) => resource.id === `${providerRealmURL}model-4.6`, + ); + assert.ok(linkedCard, 'includes the cross-realm dotted-id card'); + assert.strictEqual( + linkedCard?.attributes?.cardTitle, + 'Model 4.6', + 'the dotted-id link resolves as a card, not a file', + ); + }); }); module('Query-backed relationships runtime resolver', function (hooks) { diff --git a/packages/runtime-common/file-def-code-ref.ts b/packages/runtime-common/file-def-code-ref.ts index 29d34671de..e492d0d1cf 100644 --- a/packages/runtime-common/file-def-code-ref.ts +++ b/packages/runtime-common/file-def-code-ref.ts @@ -83,12 +83,14 @@ function extensionOf(url: URL): string { return dot <= 0 ? '' : name.slice(dot).toLowerCase(); } -// Whether a URL names a file rather than a card. Card ids never carry a -// file extension (an instance's `.json` is stripped from its id), so an -// extension on the last path segment is a reliable tell that the URL -// targets a file. +// Whether a URL names a file rather than a card, judged by a known FileDef +// extension on the last path segment. Card ids never end in one (an +// instance's `.json` is stripped from its id) — but a bare "has a dot" +// check would misfire on card ids that legitimately contain dots (e.g. a +// `ModelConfiguration/claude-sonnet-4.6` instance), so only registered +// file extensions count. export function urlNamesFile(url: URL): boolean { - return extensionOf(url) !== ''; + return extensionOf(url) in FILEDEF_CODE_REF_BY_EXTENSION; } export function resolveFileDefCodeRef( diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index 89d8e56091..f7766afc35 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -1313,11 +1313,11 @@ export class RealmIndexQueryEngine { let entries = await Promise.all( urls.map(async (url) => { let response: Response; - // A file link (the URL carries a file extension; card ids never do) - // is requested with the file-meta mime so the serving realm returns - // the index-enriched document — consumers rely on index-derived - // attributes like a markdown skill's `kind`, which the card-mime - // fallback for file paths omits. + // A file link (the URL ends in a known FileDef extension; card ids + // never do) is requested with the file-meta mime so the serving + // realm returns the index-enriched document — consumers rely on + // index-derived attributes like a markdown skill's `kind`, which + // the card-mime fallback for file paths omits. let accept = urlNamesFile(new URL(url)) ? SupportedMimeType.FileMeta : SupportedMimeType.CardJson; @@ -1608,10 +1608,11 @@ export class RealmIndexQueryEngine { | typeof CardResourceType | typeof FileMetaResourceType | undefined; - // Card ids never carry a file extension, so the URL itself says - // whether the link targets a file. That also corrects stale index - // payloads which record file relationships as type "card" (or omit - // the type) when linked files were indexed after instances. + // Card ids never end in a known FileDef extension, so the URL + // itself says whether the link targets a file. That also corrects + // stale index payloads which record file relationships as type + // "card" (or omit the type) when linked files were indexed after + // instances. let expectsFileMeta = relationshipType === FileMetaResourceType || urlNamesFile(linkURL); let expectsCard =