mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
fix(slice-05): agent_images PRD-coherent dir-path contract (guard B1 repair on 38f4206e) (OPR.0.3.2.5)
Per velocity-guard BLOCKING-CONCERN on 38f4206e (qitem-…1dd23f63): I
codified agent_images as manifest.yaml-file paths (mirroring
context_packs), but PRD §Item 6 line 197 explicitly says:
agent_images: [path/to/agent-image-name/, ...]
Directory paths, not manifest paths. The line 203-204 routing target is
also dir-shaped (~/.openrig/agent-images/<name>/). I made an
interpretation drift without orch/founder authority.
Taking guard's preferred fix: follow PRD literally. Type-layer code
unchanged (still validates string[] of relative-safe paths). Contract
clarifications:
bundle-types.ts:
- validateAgentImagesBlock doc rewritten: "relative-safe path to an
agent-image DIRECTORY inside the bundle (per PRD §Item 6 line 197)";
"router copies the declared directory itself"; "consumer requires
manifest.yaml to exist inside each routed image dir; router enforces
at copy time"
- agentImages?: string[] field doc on both v1+v2 manifests rewritten to
match (DIRECTORIES per PRD line 197, not manifest paths)
bundle-types.test.ts (v1) + pod-bundle-assembler.test.ts (v2):
- All test fixture examples updated from "agent-images/<name>/manifest.yaml"
to "agent-images/<name>" (no trailing slash, which isRelativeSafePath
rejects as an empty path segment; PRD's "path/to/agent-image-name/"
notation is doc-style, not the literal valid path)
Step 2 router design implications:
- Router treats each declared path as the agent-image DIRECTORY
- Requires fs.isDirectory(sourceAbs) on the declared path (not parent)
- Requires fs.exists(join(sourceAbs, "manifest.yaml")) — manifest.yaml
must exist as a file inside the image dir (consumer-visibility class
per agent-image-library-service.ts:93)
- Detects basename(sourceAbs) collisions across declared images
- Copies the whole image dir to <target>/<basename(sourceAbs)>/
127/127 green across bundle-types + pod-bundle-assembler. No
algorithmic change in this commit; tests still exercise the
validator/serializer mechanics — just with PRD-coherent examples.
Tenth same-class catch on slice-05's tail. Pattern: I assumed symmetry
with context_packs without confirming against PRD authority. Banked
discipline extension: when extending a pattern to a new kind, the FIRST
check is "does PRD specify the same shape for this kind?" — not
"context_packs uses shape X so agent_images uses shape X". PRD is the
authority for field shape; consumer contract is the authority for
target shape; the two are not necessarily the same.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
38f4206ed7
commit
e7a0b253b0
3 files changed
+22
-21
No files matched your search
@@ -238,11 +238,12 @@ function normalizeContextPacksBlock(raw: unknown): string[] | 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. */
|
||||
* Each entry is a relative-safe path to an agent-image DIRECTORY inside the
|
||||
* bundle (per PRD §Item 6 line 197: agent_images: [path/to/agent-image-name/,
|
||||
* ...]). The router copies the declared directory itself to the operator
|
||||
* agent-images library on install. The consumer
|
||||
* (agent-image-library-service.ts:77-95) requires manifest.yaml to exist
|
||||
* inside each routed image dir; the router enforces that at copy time. */
|
||||
function validateAgentImagesBlock(raw: unknown, errors: string[]): void {
|
||||
if (raw === undefined) return;
|
||||
if (!Array.isArray(raw)) {
|
||||
@@ -391,7 +392,7 @@ 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). */
|
||||
/** Item 6 cross-primitive bundling: paths to agent-image DIRECTORIES (per PRD §Item 6 line 197); router copies the declared directory to the operator agent-images library on install. Consumer requires manifest.yaml inside each image dir. (Checkpoint 7.3g) */
|
||||
agentImages?: string[];
|
||||
}
|
||||
|
||||
@@ -506,7 +507,7 @@ 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). */
|
||||
/** Item 6 cross-primitive bundling: paths to agent-image DIRECTORIES (per PRD §Item 6 line 197); router copies the declared directory to the operator agent-images library on install. Consumer requires manifest.yaml inside each image dir. (Checkpoint 7.3g) */
|
||||
agentImages?: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -697,27 +697,27 @@ describe("Bundle types", () => {
|
||||
});
|
||||
|
||||
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 raw = { ...VALID_RAW, agent_images: ["agent-images/seat-a", "agent-images/seat-b"] };
|
||||
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 raw = { ...VALID_RAW, agent_images: "agent-images/x" };
|
||||
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 raw = { ...VALID_RAW, agent_images: [99, "agent-images/ok"] };
|
||||
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 raw = { ...VALID_RAW, agent_images: ["../escape"] };
|
||||
const result = validateBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.includes("agent_images[0]"))).toBe(true);
|
||||
@@ -736,17 +736,17 @@ describe("Bundle types", () => {
|
||||
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"],
|
||||
agentImages: ["agent-images/seat-a", "agent-images/seat-b"],
|
||||
};
|
||||
const yaml = serializeBundleManifest(manifest);
|
||||
expect(yaml).toContain("agent_images:");
|
||||
expect(yaml).toContain("agent-images/seat-a/manifest.yaml");
|
||||
expect(yaml).toContain("agent-images/seat-a");
|
||||
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"]);
|
||||
expect(normalized.agentImages).toEqual(["agent-images/seat-a", "agent-images/seat-b"]);
|
||||
});
|
||||
|
||||
it("missing agent_images round-trips cleanly (no field emitted in YAML)", () => {
|
||||
|
||||
@@ -1111,7 +1111,7 @@ describe("PodBundleManifest validation", () => {
|
||||
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"],
|
||||
agent_images: ["agent-images/seat-a", "agent-images/seat-b"],
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(true);
|
||||
@@ -1122,7 +1122,7 @@ describe("PodBundleManifest validation", () => {
|
||||
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",
|
||||
agent_images: "agent-images/a",
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -1134,7 +1134,7 @@ describe("PodBundleManifest validation", () => {
|
||||
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"],
|
||||
agent_images: [99, "agent-images/ok"],
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -1146,7 +1146,7 @@ describe("PodBundleManifest validation", () => {
|
||||
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"],
|
||||
agent_images: ["../escape"],
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -1162,16 +1162,16 @@ describe("PodBundleManifest validation", () => {
|
||||
originalRef: "local:agents/impl", hash: "def",
|
||||
importEntries: [],
|
||||
}],
|
||||
agentImages: ["agent-images/v2-seat-a/manifest.yaml", "agent-images/v2-seat-b/manifest.yaml"],
|
||||
agentImages: ["agent-images/v2-seat-a", "agent-images/v2-seat-b"],
|
||||
};
|
||||
const yaml = serializePodBundleManifest(manifest);
|
||||
expect(yaml).toContain("agent_images:");
|
||||
expect(yaml).toContain("agent-images/v2-seat-a/manifest.yaml");
|
||||
expect(yaml).toContain("agent-images/v2-seat-a");
|
||||
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"]);
|
||||
expect(m["agent_images"]).toEqual(["agent-images/v2-seat-a", "agent-images/v2-seat-b"]);
|
||||
});
|
||||
|
||||
it("v2: round-trip preserves compatibility through serialize -> parse", () => {
|
||||
|
||||
Reference in new issue
Block a user