Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 74 additions & 6 deletions pkgs/agent-package/src/archive-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ interface PackageAssetReadResult {
issues: AgentResolutionIssue[];
}

interface SkillAssetBucket {
entries: Array<[path: string, contentBytes: Uint8Array]>;
skill: AgentPackage["manifest"]["skills"][number];
skillPath: string;
}

export function readPackageAssets(
agentPackage: AgentPackage,
entries: Record<string, Uint8Array>,
Expand All @@ -30,12 +36,10 @@ function readSkillAssets(
assets: AgentPackageAsset[],
issues: AgentResolutionIssue[],
): void {
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,
);

for (const { entries: skillEntries, skill, skillPath } of collectSkillAssetBuckets(
agentPackage,
entries,
)) {
if (skillEntries.length === 0) {
issues.push(
createArchiveIssue({
Expand Down Expand Up @@ -75,3 +79,67 @@ function readSkillAssets(
}
}
}

function collectSkillAssetBuckets(
agentPackage: AgentPackage,
entries: Record<string, Uint8Array>,
): SkillAssetBucket[] {
const buckets: SkillAssetBucket[] = agentPackage.manifest.skills.map((skill) => ({
entries: [],
skill,
skillPath: skill.skillId.endsWith("/") ? skill.skillId : `${skill.skillId}/`,
}));

if (buckets.length === 0) {
return buckets;
}

const archiveEntries = Object.entries(entries);

if (buckets.length === 1) {
const [bucket] = buckets;

if (bucket !== undefined) {
bucket.entries = archiveEntries.filter(
([path, contentBytes]) => contentBytes.byteLength > 0 && path.startsWith(bucket.skillPath),
);
}

return buckets;
}

const entriesBySkillPath = new Map<string, SkillAssetBucket["entries"]>();

for (const bucket of buckets) {
if (!entriesBySkillPath.has(bucket.skillPath)) {
entriesBySkillPath.set(bucket.skillPath, []);
}
}

for (const [path, contentBytes] of archiveEntries) {
if (contentBytes.byteLength === 0) {
continue;
}

// Probe directory prefixes once per entry so overlapping and duplicate
// skill declarations keep their existing matching behavior.
let slashIndex = path.indexOf("/");

while (slashIndex !== -1) {
const candidateRoot = path.slice(0, slashIndex + 1);
const matchingEntries = entriesBySkillPath.get(candidateRoot);

if (matchingEntries !== undefined) {
matchingEntries.push([path, contentBytes]);
}

slashIndex = path.indexOf("/", slashIndex + 1);
}
}

for (const bucket of buckets) {
bucket.entries = entriesBySkillPath.get(bucket.skillPath) ?? [];
}

return buckets;
}
52 changes: 52 additions & 0 deletions pkgs/agent-package/tests/archive-entry-admission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import type { AgentPackage } from "@mosoo/contracts/agent-manifest";
import { AGENT_MANIFEST_VERSION, AGENT_PACKAGE_VERSION } from "@mosoo/contracts/agent-manifest";

import { readPackageAssets } from "../src/archive-assets";

interface StoredZipEntry {
body: Uint8Array;
path: string;
Expand Down Expand Up @@ -236,6 +238,56 @@ describe("agent package archive entry admission", () => {
]);
});

test("indexes skill assets without changing declaration or entry order", () => {
const agentPackage = createAgentPackageFixture({
skills: [
{
ownerName: null,
skillId: "skills/demo/nested/",
skillName: "Nested",
state: "active",
},
{
ownerName: null,
skillId: "skills/demo/",
skillName: "Demo",
state: "active",
},
{
ownerName: null,
skillId: "skills/demo/",
skillName: "Demo duplicate",
state: "active",
},
{
ownerName: null,
skillId: "skills/empty/",
skillName: "Empty",
state: "active",
},
],
});
const result = readPackageAssets(agentPackage, {
"skills/demo/nested/child.txt": textToArchiveBytes("child"),
"skills/demo/root.txt": textToArchiveBytes("root"),
"skills/empty/zero.txt": new Uint8Array(),
});

expect(result.assets.map((asset) => ({ filename: asset.filename, key: asset.key }))).toEqual([
{ filename: "child.txt", key: "skills/demo/nested/child.txt" },
{ filename: "nested/child.txt", key: "skills/demo/nested/child.txt" },
{ filename: "root.txt", key: "skills/demo/root.txt" },
{ filename: "nested/child.txt", key: "skills/demo/nested/child.txt" },
{ filename: "root.txt", key: "skills/demo/root.txt" },
]);
expect(result.issues).toEqual([
expect.objectContaining({
code: "package.skill.missing",
targetLabel: "Empty",
}),
]);
});

test("rejects package assets that point at a declared skill root", () => {
const parsed = parseAgentPackageArchiveBytes(
createStoredZipArchive([
Expand Down
Loading