mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
feat(slice-05): context_packs manifest type extension — Item 6 cross-primitive bundling part 5a (Checkpoint 7.3f step 1) (OPR.0.3.2.5)
4th cross-primitive kind per orch ruling Candidate A. Mirrors the workflow_specs type extension (4f50d127+da286abd+1e4b229d): same pattern (string[] of relative-safe paths) but for context-pack manifest.yaml paths. The router (lands at 7.3f step 2) will copy the parent directory of each manifest.yaml to the operator context-packs library. Per PRD §Item 6 line 196: `context_packs: [path/to/manifest.yaml, ...]` — entries are paths to context-pack manifest.yaml files inside the bundle. Consumer is context-pack-library-service.scan() which walks roots whose immediate children are pack dirs (each containing a manifest.yaml). Type-layer additions (mirroring workflow_specs pattern): - validateContextPacksBlock: array-typing + per-entry string + isRelativeSafePath - normalizeContextPacksBlock: defensive copy + string filter + empty-undefined - Field added to BOTH PodBundleManifest v2 (contextPacks?: string[]) + LegacyBundleManifest v1 - Validators wired into validatePodBundleManifest + validateLegacyBundleManifest - Normalizer wired into normalizeLegacyBundleManifest - Serializers wired into both serializePodBundleManifest + serializeLegacyBundleManifest Tests PROACTIVELY include FULL v1 AND v2 coverage in the same commit (avoiding the4f50d127cycle where v2 coverage came in a follow-up B1 repair): v1 (bundle-types.test.ts +90): missing / valid / non-array / non-string- entry / unsafe-path / round-trip / missing-round-trips-cleanly v2 (pod-bundle-assembler.test.ts +84): missing / valid / non-array / non-string-entry / unsafe / round-trip Discriminator-flip evidence BEFORE this commit (PROACTIVELY): temporarily commented out validateContextPacksBlock call in validatePodBundleManifest. Result: 3/58 fail — exactly the v2 negative tests (non-array + non-string-entry + unsafe-path). Positive tests (missing/valid/round-trip) stay green. Proves the v2 validator wiring is load-bearing for the negative branches. Restored, 114/114 green across bundle-types + pod-bundle-assembler. Item-6 progress: 7.3e workflow_specs DONE (37 commits over main). After 7.3f context_packs (this commit step 1 → step 2 router → step 3 /install integration with degenerate-input dogfood), then 7.3g agent_images, then 7.4 gstack proof case. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
16ebb8af07
commit
329e2e228d
3 files changed
+200
No files matched your search
@@ -205,6 +205,38 @@ function normalizeWorkflowSpecsBlock(raw: unknown): string[] | undefined {
|
||||
return result.length > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
/** Validate optional context_packs[] block. Item 6 / slice-05 Checkpoint 7.3f.
|
||||
* Each entry is a relative-safe path to a context-pack's manifest.yaml inside
|
||||
* the bundle (per PRD §Item 6). The router copies the parent directory of
|
||||
* each manifest path to the operator context-packs library on install. */
|
||||
function validateContextPacksBlock(raw: unknown, errors: string[]): void {
|
||||
if (raw === undefined) return;
|
||||
if (!Array.isArray(raw)) {
|
||||
errors.push("context_packs must be an array");
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
const entry = raw[i];
|
||||
if (typeof entry !== "string") {
|
||||
errors.push(`context_packs[${i}] must be a string`);
|
||||
continue;
|
||||
}
|
||||
if (!isRelativeSafePath(entry)) {
|
||||
errors.push(`context_packs[${i}] path is not safe: '${entry}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalize raw context_packs[] block (defensive copy + string filter). */
|
||||
function normalizeContextPacksBlock(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
|
||||
@@ -323,6 +355,8 @@ export interface PodBundleManifest {
|
||||
plugins?: BundlePluginReference[];
|
||||
/** Item 6 cross-primitive bundling: workflow spec YAML paths to route to the operator workflow-specs library on install (Checkpoint 7.3e). */
|
||||
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[];
|
||||
}
|
||||
|
||||
export function validatePodBundleManifest(raw: unknown): { valid: boolean; errors: string[] } {
|
||||
@@ -354,6 +388,7 @@ export function validatePodBundleManifest(raw: unknown): { valid: boolean; error
|
||||
validateSkillsBlock(m["skills"], errors);
|
||||
validatePluginsBlock(m["plugins"], errors);
|
||||
validateWorkflowSpecsBlock(m["workflow_specs"], errors);
|
||||
validateContextPacksBlock(m["context_packs"], errors);
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
@@ -387,6 +422,7 @@ export function serializePodBundleManifest(manifest: PodBundleManifest): string
|
||||
if (manifest.skills && manifest.skills.length > 0) doc["skills"] = manifest.skills;
|
||||
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;
|
||||
return stringifyYaml(doc);
|
||||
}
|
||||
|
||||
@@ -430,6 +466,8 @@ export interface LegacyBundleManifest {
|
||||
plugins?: BundlePluginReference[];
|
||||
/** Item 6 cross-primitive bundling: workflow spec YAML paths to route to the operator workflow-specs library on install (Checkpoint 7.3e). */
|
||||
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[];
|
||||
}
|
||||
|
||||
/** Validation options */
|
||||
@@ -525,6 +563,7 @@ export function validateLegacyBundleManifest(
|
||||
validateSkillsBlock(m["skills"], errors);
|
||||
validatePluginsBlock(m["plugins"], errors);
|
||||
validateWorkflowSpecsBlock(m["workflow_specs"], errors);
|
||||
validateContextPacksBlock(m["context_packs"], errors);
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
@@ -582,6 +621,9 @@ export function normalizeLegacyBundleManifest(raw: unknown): LegacyBundleManifes
|
||||
const workflowSpecs = normalizeWorkflowSpecsBlock(m["workflow_specs"]);
|
||||
if (workflowSpecs) result.workflowSpecs = workflowSpecs;
|
||||
|
||||
const contextPacks = normalizeContextPacksBlock(m["context_packs"]);
|
||||
if (contextPacks) result.contextPacks = contextPacks;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -616,6 +658,7 @@ export function serializeLegacyBundleManifest(manifest: LegacyBundleManifest): s
|
||||
if (manifest.skills && manifest.skills.length > 0) doc["skills"] = manifest.skills;
|
||||
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;
|
||||
|
||||
return stringifyYaml(doc);
|
||||
}
|
||||
@@ -610,6 +610,83 @@ describe("Bundle types", () => {
|
||||
const normalized = normalizeBundleManifest(parsed);
|
||||
expect(normalized.workflowSpecs).toBeUndefined();
|
||||
});
|
||||
|
||||
// -- Item 6 context_packs block tests (slice-05 Checkpoint 7.3f) --
|
||||
|
||||
it("missing context_packs block passes validation (backward compat)", () => {
|
||||
const raw = { ...VALID_RAW };
|
||||
const result = validateBundleManifest(raw);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("context_packs as a string array of safe relative paths passes validation", () => {
|
||||
const raw = { ...VALID_RAW, context_packs: ["context-packs/intent/manifest.yaml", "context-packs/persona/manifest.yaml"] };
|
||||
const result = validateBundleManifest(raw);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("context_packs as a non-array rejected", () => {
|
||||
const raw = { ...VALID_RAW, context_packs: "context-packs/intent/manifest.yaml" };
|
||||
const result = validateBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.includes("context_packs must be an array"))).toBe(true);
|
||||
});
|
||||
|
||||
it("non-string context_packs entry rejected", () => {
|
||||
const raw = { ...VALID_RAW, context_packs: [42, "context-packs/ok/manifest.yaml"] };
|
||||
const result = validateBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.includes("context_packs[0]") && e.includes("must be a string"))).toBe(true);
|
||||
});
|
||||
|
||||
it("unsafe context_packs path (dot-dot traversal) rejected", () => {
|
||||
const raw = { ...VALID_RAW, context_packs: ["../escape/manifest.yaml"] };
|
||||
const result = validateBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.includes("context_packs[0]"))).toBe(true);
|
||||
expect(result.errors.some((e) => e.includes("not safe"))).toBe(true);
|
||||
});
|
||||
|
||||
it("round-trip preserves context_packs through serialize → parse → normalize", () => {
|
||||
const manifest: BundleManifest = {
|
||||
schemaVersion: 1,
|
||||
name: "rt-context-packs",
|
||||
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": "c".repeat(64), "packages/pkg/package.yaml": "d".repeat(64) },
|
||||
},
|
||||
contextPacks: ["context-packs/intent/manifest.yaml", "context-packs/persona/manifest.yaml"],
|
||||
};
|
||||
const yaml = serializeBundleManifest(manifest);
|
||||
expect(yaml).toContain("context_packs:");
|
||||
expect(yaml).toContain("context-packs/intent/manifest.yaml");
|
||||
const parsed = parseBundleManifest(yaml);
|
||||
const validation = validateBundleManifest(parsed);
|
||||
expect(validation.valid).toBe(true);
|
||||
const normalized = normalizeBundleManifest(parsed);
|
||||
expect(normalized.contextPacks).toBeDefined();
|
||||
expect(normalized.contextPacks).toEqual(["context-packs/intent/manifest.yaml", "context-packs/persona/manifest.yaml"]);
|
||||
});
|
||||
|
||||
it("missing context_packs round-trips cleanly (no field emitted in YAML)", () => {
|
||||
const manifest: BundleManifest = {
|
||||
schemaVersion: 1,
|
||||
name: "no-context-packs",
|
||||
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("context_packs:");
|
||||
const parsed = parseBundleManifest(yaml);
|
||||
const normalized = normalizeBundleManifest(parsed);
|
||||
expect(normalized.contextPacks).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRelativeSafePath", () => {
|
||||
|
||||
@@ -1014,6 +1014,86 @@ describe("PodBundleManifest validation", () => {
|
||||
expect(ps[0]!["id"]).toBe("co-plugin");
|
||||
});
|
||||
|
||||
// -- Item 6 context_packs block tests for v2 (slice-05 Checkpoint 7.3f) --
|
||||
|
||||
it("v2: missing context_packs block passes validation (backward compat)", () => {
|
||||
const raw = {
|
||||
schema_version: 2, name: "no-context-packs", 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: context_packs as string array passes validation", () => {
|
||||
const raw = {
|
||||
schema_version: 2, name: "with-context-packs", 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: [] }],
|
||||
context_packs: ["context-packs/intent/manifest.yaml", "context-packs/persona/manifest.yaml"],
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("v2: context_packs as non-array rejected", () => {
|
||||
const raw = {
|
||||
schema_version: 2, name: "bad-context-packs", 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: [] }],
|
||||
context_packs: "context-packs/a/manifest.yaml",
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.includes("context_packs must be an array"))).toBe(true);
|
||||
});
|
||||
|
||||
it("v2: non-string context_packs entry rejected", () => {
|
||||
const raw = {
|
||||
schema_version: 2, name: "bad-context-packs", 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: [] }],
|
||||
context_packs: [42, "context-packs/ok/manifest.yaml"],
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.includes("context_packs[0]") && e.includes("must be a string"))).toBe(true);
|
||||
});
|
||||
|
||||
it("v2: unsafe context_packs path rejected", () => {
|
||||
const raw = {
|
||||
schema_version: 2, name: "bad-context-packs", 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: [] }],
|
||||
context_packs: ["../escape/manifest.yaml"],
|
||||
};
|
||||
const result = validatePodBundleManifest(raw);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some((e) => e.includes("context_packs[0]") && e.includes("not safe"))).toBe(true);
|
||||
});
|
||||
|
||||
it("v2: round-trip preserves context_packs through serialize -> parse", () => {
|
||||
const manifest: PodBundleManifest = {
|
||||
schemaVersion: 2, name: "rt-context-packs-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: [],
|
||||
}],
|
||||
contextPacks: ["context-packs/v2-intent/manifest.yaml", "context-packs/v2-persona/manifest.yaml"],
|
||||
};
|
||||
const yaml = serializePodBundleManifest(manifest);
|
||||
expect(yaml).toContain("context_packs:");
|
||||
expect(yaml).toContain("context-packs/v2-intent/manifest.yaml");
|
||||
const parsed = parsePodBundleManifest(yaml);
|
||||
const validation = validatePodBundleManifest(parsed);
|
||||
expect(validation.valid).toBe(true);
|
||||
const m = parsed as Record<string, unknown>;
|
||||
expect(m["context_packs"]).toEqual(["context-packs/v2-intent/manifest.yaml", "context-packs/v2-persona/manifest.yaml"]);
|
||||
});
|
||||
|
||||
it("v2: round-trip preserves compatibility through serialize -> parse", () => {
|
||||
const manifest: PodBundleManifest = {
|
||||
schemaVersion: 2, name: "rt-compat", version: "2.0",
|
||||
|
||||
Reference in new issue
Block a user