feat(slice-05): agent_images manifest type extension — Item 6 cross-primitive bundling part 6a (Checkpoint 7.3g step 1) (OPR.0.3.2.5)

5th and FINAL cross-primitive kind per orch Candidate A. Mirrors the
context_packs type extension exactly (structurally identical: paths to
manifest.yaml files of agent-image dirs).

Per PRD §Item 6 line 197: `agent_images: [path/to/agent-image-name/, ...]`
— interpreted per consumer contract (agent-image-library-service.ts:77-95
scans pack DIRS with manifest.yaml file at root) as paths to the agent-
image's manifest.yaml file. Router (lands at 7.3g step 2) will copy the
parent dir of each manifest path to <openrigHome>/agent-images/<dirname>/.

Type-layer additions (mirroring context_packs pattern):
- validateAgentImagesBlock + normalizeAgentImagesBlock (same shape as
  context_packs: string[], isRelativeSafePath, defensive copy)
- agentImages?: string[] field on BOTH PodBundleManifest v2 +
  LegacyBundleManifest v1
- Validators wired into validatePodBundleManifest +
  validateLegacyBundleManifest
- Normalizer wired into normalizeLegacyBundleManifest
- Serializers wired into both v1+v2 (snake_case key "agent_images",
  camelCase field "agentImages")

Tests PROACTIVELY include FULL v1 AND v2 coverage in the same commit:
v1 (bundle-types.test.ts +90): missing / valid / non-array / non-string /
unsafe / round-trip / missing-round-trips
v2 (pod-bundle-assembler.test.ts +84): missing / valid / non-array /
non-string / unsafe / round-trip

Discriminator-flip evidence BEFORE this commit (PROACTIVELY):
temporarily commented out validateAgentImagesBlock in
validatePodBundleManifest. Result: 3/64 fail in pod-bundle-assembler.test.ts
— exactly the v2 negative tests (non-array + non-string + unsafe).
Positive tests stay green. Proves v2 validator wiring is load-bearing.
Restored, 127/127 green across bundle-types + pod-bundle-assembler.

Consumer contract (agent-image-library-service.ts:77-95) is structurally
IDENTICAL to context-pack-library-service.scan:
- Walks root → readdirSync → isDirectory check → join "manifest.yaml" →
  existsSync → readImageEntry (readFileSync)
- Same discrimination axes: dir-vs-file at pack level, file-vs-dir at
  manifest level
- All 4 banked router-level catches from context_packs (file-existence,
  isDirectory check on manifest, basename=manifest.yaml, parent-dir
  collision) apply IDENTICALLY to agent_images

Item-6 progress: 4 of 4 PRD-named kinds + 1 manifest type extension for
the 5th now done. Next: 7.3g step 2 (router; will mirror bundle-context-
packs-router structurally with all 4 banked checks built-in from start)
→ 7.3g step 3 (/install integration + real AgentImageLibraryService.scan
proof + degenerate-input probes) → 7.4 gstack HYBRID proof case (Item 6
closer).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-31 19:57:22 -07:00
1 parent cb0bf7b943
commit 38f4206ed7
3 files changed
+202

No files matched your search

@@ -237,6 +237,40 @@ function normalizeContextPacksBlock(raw: unknown): string[] | undefined {
return result.length > 0 ? result : undefined;
}
/** Validate optional agent_images[] block. Item 6 / slice-05 Checkpoint 7.3g.
* Each entry is a relative-safe path to an agent-image's manifest.yaml inside
* the bundle (per PRD §Item 6 line 197 — agent_images: [path/to/agent-image-
* name/, ...] — interpreted as the manifest.yaml path per the consumer
* contract). The router copies the parent directory of each manifest path to
* the operator agent-images library on install. */
function validateAgentImagesBlock(raw: unknown, errors: string[]): void {
if (raw === undefined) return;
if (!Array.isArray(raw)) {
errors.push("agent_images must be an array");
return;
}
for (let i = 0; i < raw.length; i++) {
const entry = raw[i];
if (typeof entry !== "string") {
errors.push(`agent_images[${i}] must be a string`);
continue;
}
if (!isRelativeSafePath(entry)) {
errors.push(`agent_images[${i}] path is not safe: '${entry}'`);
}
}
}
/** Normalize raw agent_images[] block (defensive copy + string filter). */
function normalizeAgentImagesBlock(raw: unknown): string[] | undefined {
if (!Array.isArray(raw)) return undefined;
const result: string[] = [];
for (const entry of raw) {
if (typeof entry === "string" && entry.length > 0) result.push(entry);
}
return result.length > 0 ? result : undefined;
}
/**
* Compatibility block — operator-declared install-time requirements for a
* bundle artifact. All fields optional; missing block keeps backward compat
@@ -357,6 +391,8 @@ export interface PodBundleManifest {
workflowSpecs?: string[];
/** Item 6 cross-primitive bundling: paths to context-pack manifest.yaml files; router copies the parent dir to the operator context-packs library on install (Checkpoint 7.3f). */
contextPacks?: string[];
/** Item 6 cross-primitive bundling: paths to agent-image manifest.yaml files; router copies the parent dir to the operator agent-images library on install (Checkpoint 7.3g). */
agentImages?: string[];
}
export function validatePodBundleManifest(raw: unknown): { valid: boolean; errors: string[] } {
@@ -389,6 +425,7 @@ export function validatePodBundleManifest(raw: unknown): { valid: boolean; error
validatePluginsBlock(m["plugins"], errors);
validateWorkflowSpecsBlock(m["workflow_specs"], errors);
validateContextPacksBlock(m["context_packs"], errors);
validateAgentImagesBlock(m["agent_images"], errors);
return { valid: errors.length === 0, errors };
}
@@ -423,6 +460,7 @@ export function serializePodBundleManifest(manifest: PodBundleManifest): string
if (manifest.plugins && manifest.plugins.length > 0) doc["plugins"] = manifest.plugins.map((p) => ({ id: p.id, source: { kind: p.source.kind, path: p.source.path } }));
if (manifest.workflowSpecs && manifest.workflowSpecs.length > 0) doc["workflow_specs"] = manifest.workflowSpecs;
if (manifest.contextPacks && manifest.contextPacks.length > 0) doc["context_packs"] = manifest.contextPacks;
if (manifest.agentImages && manifest.agentImages.length > 0) doc["agent_images"] = manifest.agentImages;
return stringifyYaml(doc);
}
@@ -468,6 +506,8 @@ export interface LegacyBundleManifest {
workflowSpecs?: string[];
/** Item 6 cross-primitive bundling: paths to context-pack manifest.yaml files; router copies the parent dir to the operator context-packs library on install (Checkpoint 7.3f). */
contextPacks?: string[];
/** Item 6 cross-primitive bundling: paths to agent-image manifest.yaml files; router copies the parent dir to the operator agent-images library on install (Checkpoint 7.3g). */
agentImages?: string[];
}
/** Validation options */
@@ -564,6 +604,7 @@ export function validateLegacyBundleManifest(
validatePluginsBlock(m["plugins"], errors);
validateWorkflowSpecsBlock(m["workflow_specs"], errors);
validateContextPacksBlock(m["context_packs"], errors);
validateAgentImagesBlock(m["agent_images"], errors);
return { valid: errors.length === 0, errors };
}
@@ -624,6 +665,9 @@ export function normalizeLegacyBundleManifest(raw: unknown): LegacyBundleManifes
const contextPacks = normalizeContextPacksBlock(m["context_packs"]);
if (contextPacks) result.contextPacks = contextPacks;
const agentImages = normalizeAgentImagesBlock(m["agent_images"]);
if (agentImages) result.agentImages = agentImages;
return result;
}
@@ -659,6 +703,7 @@ export function serializeLegacyBundleManifest(manifest: LegacyBundleManifest): s
if (manifest.plugins && manifest.plugins.length > 0) doc["plugins"] = manifest.plugins.map((p) => ({ id: p.id, source: { kind: p.source.kind, path: p.source.path } }));
if (manifest.workflowSpecs && manifest.workflowSpecs.length > 0) doc["workflow_specs"] = manifest.workflowSpecs;
if (manifest.contextPacks && manifest.contextPacks.length > 0) doc["context_packs"] = manifest.contextPacks;
if (manifest.agentImages && manifest.agentImages.length > 0) doc["agent_images"] = manifest.agentImages;
return stringifyYaml(doc);
}
+77
View File
@@ -687,6 +687,83 @@ describe("Bundle types", () => {
const normalized = normalizeBundleManifest(parsed);
expect(normalized.contextPacks).toBeUndefined();
});
// -- Item 6 agent_images block tests (slice-05 Checkpoint 7.3g) --
it("missing agent_images block passes validation (backward compat)", () => {
const raw = { ...VALID_RAW };
const result = validateBundleManifest(raw);
expect(result.valid).toBe(true);
});
it("agent_images as a string array of safe relative paths passes validation", () => {
const raw = { ...VALID_RAW, agent_images: ["agent-images/seat-a/manifest.yaml", "agent-images/seat-b/manifest.yaml"] };
const result = validateBundleManifest(raw);
expect(result.valid).toBe(true);
});
it("agent_images as a non-array rejected", () => {
const raw = { ...VALID_RAW, agent_images: "agent-images/x/manifest.yaml" };
const result = validateBundleManifest(raw);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("agent_images must be an array"))).toBe(true);
});
it("non-string agent_images entry rejected", () => {
const raw = { ...VALID_RAW, agent_images: [99, "agent-images/ok/manifest.yaml"] };
const result = validateBundleManifest(raw);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("agent_images[0]") && e.includes("must be a string"))).toBe(true);
});
it("unsafe agent_images path (dot-dot traversal) rejected", () => {
const raw = { ...VALID_RAW, agent_images: ["../escape/manifest.yaml"] };
const result = validateBundleManifest(raw);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("agent_images[0]"))).toBe(true);
expect(result.errors.some((e) => e.includes("not safe"))).toBe(true);
});
it("round-trip preserves agent_images through serialize → parse → normalize", () => {
const manifest: BundleManifest = {
schemaVersion: 1,
name: "rt-agent-images",
version: "1.0.0",
createdAt: "2026-05-18T12:00:00Z",
rigSpec: "rig.yaml",
packages: [{ name: "pkg", version: "1.0.0", path: "packages/pkg", originalSource: "local:./pkg" }],
integrity: {
algorithm: "sha256",
files: { "rig.yaml": "e".repeat(64), "packages/pkg/package.yaml": "f".repeat(64) },
},
agentImages: ["agent-images/seat-a/manifest.yaml", "agent-images/seat-b/manifest.yaml"],
};
const yaml = serializeBundleManifest(manifest);
expect(yaml).toContain("agent_images:");
expect(yaml).toContain("agent-images/seat-a/manifest.yaml");
const parsed = parseBundleManifest(yaml);
const validation = validateBundleManifest(parsed);
expect(validation.valid).toBe(true);
const normalized = normalizeBundleManifest(parsed);
expect(normalized.agentImages).toBeDefined();
expect(normalized.agentImages).toEqual(["agent-images/seat-a/manifest.yaml", "agent-images/seat-b/manifest.yaml"]);
});
it("missing agent_images round-trips cleanly (no field emitted in YAML)", () => {
const manifest: BundleManifest = {
schemaVersion: 1,
name: "no-agent-images",
version: "1.0.0",
createdAt: "2026-05-18T00:00:00Z",
rigSpec: "rig.yaml",
packages: [{ name: "pkg", version: "1.0", path: "packages/pkg", originalSource: "local:./pkg" }],
};
const yaml = serializeBundleManifest(manifest);
expect(yaml).not.toContain("agent_images:");
const parsed = parseBundleManifest(yaml);
const normalized = normalizeBundleManifest(parsed);
expect(normalized.agentImages).toBeUndefined();
});
});
describe("isRelativeSafePath", () => {
@@ -1094,6 +1094,86 @@ describe("PodBundleManifest validation", () => {
expect(m["context_packs"]).toEqual(["context-packs/v2-intent/manifest.yaml", "context-packs/v2-persona/manifest.yaml"]);
});
// -- Item 6 agent_images block tests for v2 (slice-05 Checkpoint 7.3g) --
it("v2: missing agent_images block passes validation (backward compat)", () => {
const raw = {
schema_version: 2, name: "no-agent-images", version: "1.0",
created_at: "2026-05-18T00:00:00Z", rig_spec: "rig.yaml",
agents: [{ name: "impl", version: "1.0", path: "agents/impl", original_ref: "local:agents/impl", hash: "abc", import_entries: [] }],
};
const result = validatePodBundleManifest(raw);
expect(result.valid).toBe(true);
});
it("v2: agent_images as string array passes validation", () => {
const raw = {
schema_version: 2, name: "with-agent-images", version: "1.0",
created_at: "2026-05-18T00:00:00Z", rig_spec: "rig.yaml",
agents: [{ name: "impl", version: "1.0", path: "agents/impl", original_ref: "local:agents/impl", hash: "abc", import_entries: [] }],
agent_images: ["agent-images/seat-a/manifest.yaml", "agent-images/seat-b/manifest.yaml"],
};
const result = validatePodBundleManifest(raw);
expect(result.valid).toBe(true);
});
it("v2: agent_images as non-array rejected", () => {
const raw = {
schema_version: 2, name: "bad-agent-images", version: "1.0",
created_at: "2026-05-18T00:00:00Z", rig_spec: "rig.yaml",
agents: [{ name: "impl", version: "1.0", path: "agents/impl", original_ref: "local:agents/impl", hash: "abc", import_entries: [] }],
agent_images: "agent-images/a/manifest.yaml",
};
const result = validatePodBundleManifest(raw);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("agent_images must be an array"))).toBe(true);
});
it("v2: non-string agent_images entry rejected", () => {
const raw = {
schema_version: 2, name: "bad-agent-images", version: "1.0",
created_at: "2026-05-18T00:00:00Z", rig_spec: "rig.yaml",
agents: [{ name: "impl", version: "1.0", path: "agents/impl", original_ref: "local:agents/impl", hash: "abc", import_entries: [] }],
agent_images: [99, "agent-images/ok/manifest.yaml"],
};
const result = validatePodBundleManifest(raw);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("agent_images[0]") && e.includes("must be a string"))).toBe(true);
});
it("v2: unsafe agent_images path rejected", () => {
const raw = {
schema_version: 2, name: "bad-agent-images", version: "1.0",
created_at: "2026-05-18T00:00:00Z", rig_spec: "rig.yaml",
agents: [{ name: "impl", version: "1.0", path: "agents/impl", original_ref: "local:agents/impl", hash: "abc", import_entries: [] }],
agent_images: ["../escape/manifest.yaml"],
};
const result = validatePodBundleManifest(raw);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("agent_images[0]") && e.includes("not safe"))).toBe(true);
});
it("v2: round-trip preserves agent_images through serialize -> parse", () => {
const manifest: PodBundleManifest = {
schemaVersion: 2, name: "rt-agent-images-v2", version: "2.0",
createdAt: "2026-05-18T00:00:00Z", rigSpec: "rig.yaml",
agents: [{
name: "impl", version: "1.0", path: "agents/impl",
originalRef: "local:agents/impl", hash: "def",
importEntries: [],
}],
agentImages: ["agent-images/v2-seat-a/manifest.yaml", "agent-images/v2-seat-b/manifest.yaml"],
};
const yaml = serializePodBundleManifest(manifest);
expect(yaml).toContain("agent_images:");
expect(yaml).toContain("agent-images/v2-seat-a/manifest.yaml");
const parsed = parsePodBundleManifest(yaml);
const validation = validatePodBundleManifest(parsed);
expect(validation.valid).toBe(true);
const m = parsed as Record<string, unknown>;
expect(m["agent_images"]).toEqual(["agent-images/v2-seat-a/manifest.yaml", "agent-images/v2-seat-b/manifest.yaml"]);
});
it("v2: round-trip preserves compatibility through serialize -> parse", () => {
const manifest: PodBundleManifest = {
schemaVersion: 2, name: "rt-compat", version: "2.0",