diff --git a/pkgs/agent-package/src/archive-assets.ts b/pkgs/agent-package/src/archive-assets.ts index db559297..9318689a 100644 --- a/pkgs/agent-package/src/archive-assets.ts +++ b/pkgs/agent-package/src/archive-assets.ts @@ -12,6 +12,12 @@ interface PackageAssetReadResult { issues: AgentResolutionIssue[]; } +type ArchiveEntry = [path: string, contentBytes: Uint8Array]; + +function toSkillPath(skillId: string): string { + return skillId.endsWith("/") ? skillId : `${skillId}/`; +} + export function readPackageAssets( agentPackage: AgentPackage, entries: Record, @@ -30,11 +36,11 @@ function readSkillAssets( assets: AgentPackageAsset[], issues: AgentResolutionIssue[], ): void { + const skillEntriesByPath = indexSkillEntries(agentPackage, entries); + for (const skill of agentPackage.manifest.skills) { - const skillPath = skill.skillId.endsWith("/") ? skill.skillId : `${skill.skillId}/`; - const skillEntries = Object.entries(entries).filter( - ([path, entry]) => path.startsWith(skillPath) && entry.byteLength > 0, - ); + const skillPath = toSkillPath(skill.skillId); + const skillEntries = skillEntriesByPath.get(skillPath) ?? []; if (skillEntries.length === 0) { issues.push( @@ -75,3 +81,40 @@ function readSkillAssets( } } } + +function indexSkillEntries( + agentPackage: AgentPackage, + entries: Record, +): ReadonlyMap { + const entriesBySkillPath = new Map(); + + for (const skill of agentPackage.manifest.skills) { + entriesBySkillPath.set(toSkillPath(skill.skillId), []); + } + + if (entriesBySkillPath.size === 0) { + return entriesBySkillPath; + } + + for (const entry of Object.entries(entries)) { + const [path, contentBytes] = entry; + + if (contentBytes.byteLength === 0) { + continue; + } + + let slashIndex = path.indexOf("/"); + + while (slashIndex !== -1) { + const matchingEntries = entriesBySkillPath.get(path.slice(0, slashIndex + 1)); + + if (matchingEntries !== undefined) { + matchingEntries.push(entry); + } + + slashIndex = path.indexOf("/", slashIndex + 1); + } + } + + return entriesBySkillPath; +} diff --git a/pkgs/agent-package/tests/archive-entry-admission.test.ts b/pkgs/agent-package/tests/archive-entry-admission.test.ts index 87ce2bff..ec8bc1e9 100644 --- a/pkgs/agent-package/tests/archive-entry-admission.test.ts +++ b/pkgs/agent-package/tests/archive-entry-admission.test.ts @@ -236,6 +236,43 @@ describe("agent package archive entry admission", () => { ]); }); + test("preserves archive order when declared skill roots overlap", () => { + const parsed = parseAgentPackageArchiveBytes( + createStoredZipArchive([ + { + body: textToArchiveBytes( + createPackageManifestJson({ + skills: [ + { name: "Nested", path: "skills/demo/nested/" }, + { name: "Demo", path: "skills/demo/" }, + ], + }), + ), + path: "manifest.json", + }, + { + body: textToArchiveBytes('{"secretNames":[],"setupScript":""}'), + path: "environment/definition.json", + }, + { + body: textToArchiveBytes("nested"), + path: "skills/demo/nested/file.txt", + }, + { + body: textToArchiveBytes("root"), + path: "skills/demo/root.txt", + }, + ]), + ); + + expect(parsed.package).not.toBeNull(); + expect(parsed.package?.assets.map((asset) => [asset.key, asset.filename])).toEqual([ + ["skills/demo/nested/file.txt", "file.txt"], + ["skills/demo/nested/file.txt", "nested/file.txt"], + ["skills/demo/root.txt", "root.txt"], + ]); + }); + test("rejects package assets that point at a declared skill root", () => { const parsed = parseAgentPackageArchiveBytes( createStoredZipArchive([