feat(slice-05): /create auto-detects author bundle.yaml + vendors cross-primitive content (Checkpoint 7.5 / QA-20260601 A2 repair) (OPR.0.3.2.5)

Per orch ruling Candidate A on QA BLOCKING-A2 (qitem-20260601033756-
cc5fd53a): the /create route + assemblers did not consume author-
supplied bundle.yaml NOR vendor declared cross-primitive content into
the staging tree. Router tests passed because they manually tampered
the BUILT bundle.yaml after /create. The user-facing create→install
end-to-end path for gstack HYBRID / all-five-kind authoring was
broken.

Fix: auto-detect contract (no new CLI flag per orch ruling). If
bundle.yaml exists at the source root, parse + vendor + populate the
manifest with the 5 cross-primitive fields. If absent, existing
behavior unchanged (programmatic bundle.yaml from rig.yaml + packages).

consumeAuthorBundleYaml helper (bundles.ts +135):
- Reads bundle.yaml at sourceRoot (via parsePodBundleManifest — works
  for both v1+v2 author manifests since both share the snake_case
  top-level keys for the 5 kinds)
- Extracts skills / plugins / workflow_specs / context_packs / agent_images
- Per-kind vendor with both-sides containment (banked
  feedback_pre_existing_trust_boundary_reuse_canonical_helper
  addendum applied through 7.3a-g):
  * vendorFile: skills + workflow_specs (single file copy, dereferences
    symlinks → regular files for tar safety per banked pre-existing-
    trust-boundary)
  * vendorDir: plugins + agent_images (dir copy with fs.cpSync
    dereference:true → symlinks resolved to regular files at create
    time; tar safety)
  * context_packs: declared as manifest.yaml paths per PRD line 196;
    vendorDir copies the PARENT dir
- Returns AuthorBundleCrossPrimitives object; caller writes onto
  result.manifest BEFORE computeIntegrity so the vendored content +
  the manifest cross-primitive fields land in the integrity hash.

Pod-aware /create (line ~805): consumeAuthorBundleYaml call wired
between assembler.assemble and computeIntegrity. Manifest fields
populated; computeIntegrity + serializePodBundleManifest follow with
the augmented manifest. Built bundle.yaml carries the 5 fields for
the install side's routers to consume.

Legacy v1 /create (line ~880): same pattern but the legacy assembler
already writes bundle.yaml inside .assemble — so when the author
manifest has any cross-primitive content, we re-serialize bundle.yaml
via serializeLegacyBundleManifest before computeIntegrity. If no
author manifest content, no re-write needed (existing behavior intact).

End-to-end test (bundle-routes.test.ts +120):
"POST /api/bundles/create auto-detects author bundle.yaml + vendors
all 5 cross-primitive kinds (pod-aware)" — builds a real pod-aware
rig.yaml + agents/impl/agent.yaml + content for all 5 kinds at
canonical paths + author bundle.yaml declaring all 5 → POSTs /create →
asserts archive contains:
- Built bundle.yaml has all 5 cross-primitive fields (skills:,
  plugins:, id: author-plugin, workflow_specs:, context_packs:,
  agent_images:)
- Vendored on-disk content: skills/author-skill/SKILL.md,
  plugins/author-plugin/{plugin.json,README.md},
  workflows/author-flow.yaml,
  context-packs/author-pack/{manifest.yaml,brief.md},
  agent-images/author-image/manifest.yaml

Discriminator-flip evidence BEFORE this commit (PROACTIVELY):
removed the consumeAuthorBundleYaml call in the pod-aware /create
branch. Result: 1/54 fail — exactly the new test. Restored, 54/54
green across bundle-routes; 229/229 across full slice-05 daemon
suite.

Tar-safety note (banked pre-existing-trust-boundary): vendorFile +
vendorDir use fs.cpSync({dereference:true}) so author symlinks
(e.g. plugins/gstack symlinked to the substrate gstack-plugin
mirror per the cont.18 HYBRID "no fork" constraint) follow to
regular files at create time + write regular file entries into the
archive — never includes a symlink entry that tar.extract would
later treat as untrusted.

Both-sides containment: both source paths (under sourceRoot) and
target paths (under staging) checked, throwing 400-mapped errors for
escape attempts (banked addendum applied through the slice-05 tail).

After QA re-verify CLEAR on this commit's tip: slice-05 is end-to-end
shippable (Items 1-6 wiring + the create-path that produces the
manifest shape the install path routes). Then orch heavy-verify +
LOCAL --no-ff merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-31 20:50:35 -07:00
1 parent 257b51b94a
commit 79a89d4061
2 files changed
+288 -2

No files matched your search

+173 -2
View File
@@ -14,8 +14,8 @@ import { LegacyRigSpecCodec } from "../domain/rigspec-codec.js";
import { LegacyRigSpecSchema } from "../domain/rigspec-schema.js";
import { RigSpecCodec } from "../domain/rigspec-codec.js";
import { RigSpecSchema } from "../domain/rigspec-schema.js";
import { parseLegacyBundleManifest as parseBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, serializePodBundleManifest, parsePodBundleManifest, validatePodBundleManifest, validateLegacyBundleManifest, normalizeProvenanceBlock, normalizeCompatibilityBlock } from "../domain/bundle-types.js";
import type { PodBundleManifest, BundleProvenance, BundleCompatibility } from "../domain/bundle-types.js";
import { parseLegacyBundleManifest as parseBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, serializePodBundleManifest, parsePodBundleManifest, validatePodBundleManifest, validateLegacyBundleManifest, normalizeProvenanceBlock, normalizeCompatibilityBlock, isRelativeSafePath } from "../domain/bundle-types.js";
import type { PodBundleManifest, BundleProvenance, BundleCompatibility, BundlePluginReference } from "../domain/bundle-types.js";
import { fileURLToPath } from "node:url";
import { detectBundleConflicts, type BundleConflict } from "../domain/bundle-conflict-detector.js";
import type { RigRepository } from "../domain/rig-repository.js";
@@ -617,6 +617,149 @@ function writeInstallAudit(opts: {
}
}
/**
* Item 6 / slice-05 Checkpoint 7.5 (QA-20260601 A2 repair): consume an
* author-supplied bundle.yaml at the source root and vendor the
* declared cross-primitive content into the staging tree. Runs AFTER
* the assembler has built staging (rig.yaml + agents/) and BEFORE
* computeIntegrity so the vendored content lands in the integrity
* manifest.
*
* Auto-detect contract (per orch ruling, no new CLI flag): if a
* bundle.yaml exists at the source root, parse it and consume the 5
* cross-primitive fields (skills, plugins, workflow_specs,
* context_packs, agent_images). Provenance + compatibility stay
* request-body-only (caller-controlled at create time). If no
* bundle.yaml at source root, no-op (existing behavior unchanged).
*
* Vendor semantics:
* - Each declared path is resolved relative to source root.
* - Both-sides containment: source path under sourceRoot, target path
* under staging (banked
* feedback_pre_existing_trust_boundary_reuse_canonical_helper
* addendum applied through 7.3a-g).
* - Symlinks are FOLLOWED at create time and written as regular files
* to the staging tree (tar safety; banked pre-existing-trust-
* boundary lesson — never include a symlink entry in an archive).
* - Per-kind shape: skills + workflow_specs are file paths (single
* file copy); plugins + context_packs (manifest.yaml path → parent
* dir) + agent_images are dir paths (recursive copy with symlink
* dereference).
* - Throws on missing source path or path-containment violation; the
* /create route catches + returns a 400 with the message.
*
* Returns the cross-primitive blocks to populate on the manifest the
* assembler returned. The /create route writes the manifest AFTER
* populating these fields so the built bundle.yaml carries them
* for the install side to route from.
*/
interface AuthorBundleCrossPrimitives {
skills?: string[];
plugins?: BundlePluginReference[];
workflowSpecs?: string[];
contextPacks?: string[];
agentImages?: string[];
}
function consumeAuthorBundleYaml(sourceRoot: string, staging: string): AuthorBundleCrossPrimitives {
const authorBundlePath = nodePath.join(sourceRoot, "bundle.yaml");
if (!fs.existsSync(authorBundlePath)) return {};
const sourceRootResolved = nodePath.resolve(sourceRoot);
const stagingResolved = nodePath.resolve(staging);
const authorYaml = fs.readFileSync(authorBundlePath, "utf-8");
const authorParsed = parsePodBundleManifest(authorYaml) as Record<string, unknown>;
const result: AuthorBundleCrossPrimitives = {};
const vendorFile = (declared: string, kindLabel: string): void => {
if (!isRelativeSafePath(declared)) throw new Error(`author bundle ${kindLabel} path '${declared}' is not safe`);
const sourceAbs = nodePath.resolve(sourceRoot, declared);
if (sourceAbs !== sourceRootResolved && !sourceAbs.startsWith(sourceRootResolved + nodePath.sep)) {
throw new Error(`author bundle ${kindLabel} path '${declared}' escapes bundle source root`);
}
const targetAbs = nodePath.resolve(staging, declared);
if (!targetAbs.startsWith(stagingResolved + nodePath.sep)) {
throw new Error(`author bundle ${kindLabel} target '${declared}' escapes staging`);
}
if (!fs.existsSync(sourceAbs)) throw new Error(`author bundle ${kindLabel} '${declared}' does not exist in source`);
fs.mkdirSync(nodePath.dirname(targetAbs), { recursive: true });
// Dereference symlinks (tar safety per banked pre-existing-trust-boundary)
const content = fs.readFileSync(sourceAbs);
fs.writeFileSync(targetAbs, content);
};
const vendorDir = (declared: string, kindLabel: string): void => {
if (!isRelativeSafePath(declared)) throw new Error(`author bundle ${kindLabel} path '${declared}' is not safe`);
const sourceAbs = nodePath.resolve(sourceRoot, declared);
if (sourceAbs !== sourceRootResolved && !sourceAbs.startsWith(sourceRootResolved + nodePath.sep)) {
throw new Error(`author bundle ${kindLabel} path '${declared}' escapes bundle source root`);
}
const targetAbs = nodePath.resolve(staging, declared);
if (!targetAbs.startsWith(stagingResolved + nodePath.sep)) {
throw new Error(`author bundle ${kindLabel} target '${declared}' escapes staging`);
}
if (!fs.existsSync(sourceAbs)) throw new Error(`author bundle ${kindLabel} '${declared}' does not exist in source`);
fs.mkdirSync(nodePath.dirname(targetAbs), { recursive: true });
// dereference: true → symlinks are followed and written as regular files
fs.cpSync(sourceAbs, targetAbs, { recursive: true, dereference: true });
};
// skills[] — file paths
const rawSkills = authorParsed["skills"];
if (Array.isArray(rawSkills) && rawSkills.length > 0) {
const skills = rawSkills.filter((s): s is string => typeof s === "string" && s.length > 0);
for (const declared of skills) vendorFile(declared, "skill");
if (skills.length > 0) result.skills = skills;
}
// plugins[] — dir paths via source.path
const rawPlugins = authorParsed["plugins"];
if (Array.isArray(rawPlugins) && rawPlugins.length > 0) {
const plugins: BundlePluginReference[] = [];
for (const entry of rawPlugins) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
const p = entry as Record<string, unknown>;
const s = p["source"];
if (typeof p["id"] !== "string" || !p["id"]) continue;
if (!s || typeof s !== "object" || Array.isArray(s)) continue;
const src = s as Record<string, unknown>;
if (src["kind"] !== "local" || typeof src["path"] !== "string" || !src["path"]) continue;
vendorDir(src["path"], `plugin '${p["id"]}'`);
plugins.push({ id: p["id"], source: { kind: "local", path: src["path"] } });
}
if (plugins.length > 0) result.plugins = plugins;
}
// workflow_specs[] — file paths
const rawWorkflowSpecs = authorParsed["workflow_specs"];
if (Array.isArray(rawWorkflowSpecs) && rawWorkflowSpecs.length > 0) {
const workflowSpecs = rawWorkflowSpecs.filter((s): s is string => typeof s === "string" && s.length > 0);
for (const declared of workflowSpecs) vendorFile(declared, "workflow_spec");
if (workflowSpecs.length > 0) result.workflowSpecs = workflowSpecs;
}
// context_packs[] — manifest.yaml paths; vendor the parent dir
const rawContextPacks = authorParsed["context_packs"];
if (Array.isArray(rawContextPacks) && rawContextPacks.length > 0) {
const contextPacks = rawContextPacks.filter((s): s is string => typeof s === "string" && s.length > 0);
for (const declared of contextPacks) {
const parentRel = nodePath.dirname(declared);
if (parentRel === ".") continue; // declared at root; nothing meaningful to vendor
vendorDir(parentRel, `context_pack parent of '${declared}'`);
}
if (contextPacks.length > 0) result.contextPacks = contextPacks;
}
// agent_images[] — dir paths
const rawAgentImages = authorParsed["agent_images"];
if (Array.isArray(rawAgentImages) && rawAgentImages.length > 0) {
const agentImages = rawAgentImages.filter((s): s is string => typeof s === "string" && s.length > 0);
for (const declared of agentImages) vendorDir(declared, "agent_image");
if (agentImages.length > 0) result.agentImages = agentImages;
}
return result;
}
// POST /api/bundles/create
bundleRoutes.post("/create", async (c) => {
const { eventBus } = getDeps(c);
@@ -658,6 +801,17 @@ bundleRoutes.post("/create", async (c) => {
const assembler = new PodBundleAssembler({ fsOps: podAssemblerFsOps() });
const result = assembler.assemble({ rigRoot: effectiveRigRoot, rigSpecPath: nodePath.resolve(specPath), outputDir: tmpStaging, bundleName, bundleVersion, provenance, compatibility });
// Item 6 / Checkpoint 7.5 (QA-20260601 A2 repair): auto-detect
// author bundle.yaml at the rig source root; vendor declared
// cross-primitive content into staging + carry the fields onto
// the manifest. computeIntegrity below covers the vendored content.
const authorPrimitives = consumeAuthorBundleYaml(effectiveRigRoot, tmpStaging);
if (authorPrimitives.skills) result.manifest.skills = authorPrimitives.skills;
if (authorPrimitives.plugins) result.manifest.plugins = authorPrimitives.plugins;
if (authorPrimitives.workflowSpecs) result.manifest.workflowSpecs = authorPrimitives.workflowSpecs;
if (authorPrimitives.contextPacks) result.manifest.contextPacks = authorPrimitives.contextPacks;
if (authorPrimitives.agentImages) result.manifest.agentImages = authorPrimitives.agentImages;
const integrity = computeIntegrity(tmpStaging, integrityFsOps());
result.manifest.integrity = integrity;
fs.writeFileSync(nodePath.join(tmpStaging, "bundle.yaml"), serializePodBundleManifest(result.manifest), "utf-8");
@@ -716,6 +870,23 @@ bundleRoutes.post("/create", async (c) => {
specPath: nodePath.resolve(specPath), packages, outputDir: tmpStaging, bundleName, bundleVersion, provenance, compatibility,
});
// Item 6 / Checkpoint 7.5 (QA-20260601 A2 repair, legacy path mirror):
// auto-detect author bundle.yaml in source dir; vendor + carry
// cross-primitive fields before integrity. Re-serialize bundle.yaml
// since the assembler already wrote one without these fields.
const legacyAuthorPrimitives = consumeAuthorBundleYaml(specDir, tmpStaging);
const hasLegacyPrimitives = legacyAuthorPrimitives.skills || legacyAuthorPrimitives.plugins ||
legacyAuthorPrimitives.workflowSpecs || legacyAuthorPrimitives.contextPacks || legacyAuthorPrimitives.agentImages;
if (hasLegacyPrimitives) {
if (legacyAuthorPrimitives.skills) manifest.skills = legacyAuthorPrimitives.skills;
if (legacyAuthorPrimitives.plugins) manifest.plugins = legacyAuthorPrimitives.plugins;
if (legacyAuthorPrimitives.workflowSpecs) manifest.workflowSpecs = legacyAuthorPrimitives.workflowSpecs;
if (legacyAuthorPrimitives.contextPacks) manifest.contextPacks = legacyAuthorPrimitives.contextPacks;
if (legacyAuthorPrimitives.agentImages) manifest.agentImages = legacyAuthorPrimitives.agentImages;
const { serializeLegacyBundleManifest } = await import("../domain/bundle-types.js");
fs.writeFileSync(nodePath.join(tmpStaging, "bundle.yaml"), serializeLegacyBundleManifest(manifest), "utf-8");
}
const integrity = computeIntegrity(tmpStaging, integrityFsOps());
writeIntegrity(tmpStaging, integrity, integrityFsOps());
+115
View File
@@ -1533,6 +1533,121 @@ describe("Bundle API routes", () => {
}
});
// Item 6 / Checkpoint 7.5 (QA-20260601 A2 repair): /create auto-detects
// author bundle.yaml + vendors declared cross-primitive content into
// the archive + carries the cross-primitive fields onto the built
// bundle.yaml. End-to-end proof that an author can declare all 5 kinds
// and the create CLI produces a bundle that the install side will route.
it("POST /api/bundles/create auto-detects author bundle.yaml + vendors all 5 cross-primitive kinds (pod-aware)", async () => {
// 1. Build a pod-aware rig.yaml + agents/impl/agent.yaml at sourceRoot
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "create-author-bundle-src-"));
const outputPath = path.join(tmpDir, "create-author-test.rigbundle");
try {
fs.mkdirSync(path.join(sourceRoot, "agents/impl"), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, "agents/impl/agent.yaml"), [
'name: impl-agent',
'version: "1.0.0"',
'resources:',
' skills: []',
'profiles:',
' default:',
' uses:',
' skills: []',
].join("\n"));
const specPath = path.join(sourceRoot, "rig.yaml");
fs.writeFileSync(specPath, [
'version: "0.2"', 'name: author-bundle-test-rig', 'pods:',
' - id: dev', ' label: Dev', ' members:',
' - id: impl', ' agent_ref: "local:agents/impl"',
' profile: default', ' runtime: claude-code', ' cwd: .',
' edges: []', 'edges: []',
].join("\n"));
// 2. Author content for all 5 cross-primitive kinds
// skills: file
fs.mkdirSync(path.join(sourceRoot, "skills/author-skill"), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, "skills/author-skill/SKILL.md"), "# Author skill body");
// plugins: dir
fs.mkdirSync(path.join(sourceRoot, "plugins/author-plugin"), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, "plugins/author-plugin/plugin.json"), '{"name":"author-plugin","version":"1.0"}');
fs.writeFileSync(path.join(sourceRoot, "plugins/author-plugin/README.md"), "plugin readme");
// workflow_specs: file (YAML)
fs.mkdirSync(path.join(sourceRoot, "workflows"), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, "workflows/author-flow.yaml"), "workflow:\n id: author-flow\n version: '1'\n roles: { producer: {} }\n steps:\n - id: produce\n actor_role: producer\n");
// context_packs: manifest.yaml inside a dir (vendor copies parent dir)
fs.mkdirSync(path.join(sourceRoot, "context-packs/author-pack"), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, "context-packs/author-pack/manifest.yaml"), "name: author-pack\nversion: '1'\nfiles:\n - path: brief.md\n role: brief\n");
fs.writeFileSync(path.join(sourceRoot, "context-packs/author-pack/brief.md"), "# brief");
// agent_images: dir
fs.mkdirSync(path.join(sourceRoot, "agent-images/author-image"), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, "agent-images/author-image/manifest.yaml"), "name: author-image\nversion: '1'\nruntime: claude-code\nsource_seat: x@y\nsource_session_id: aaa\nsource_resume_token: aaa\ncreated_at: '2026-05-31T00:00:00Z'\nfiles: []\n");
// 3. Author bundle.yaml at sourceRoot declares all 5 kinds
fs.writeFileSync(path.join(sourceRoot, "bundle.yaml"), [
'skills:',
' - skills/author-skill/SKILL.md',
'plugins:',
' - id: author-plugin',
' source:',
' kind: local',
' path: plugins/author-plugin',
'workflow_specs:',
' - workflows/author-flow.yaml',
'context_packs:',
' - context-packs/author-pack/manifest.yaml',
'agent_images:',
' - agent-images/author-image',
].join("\n"));
// 4. /api/bundles/create — pod-aware path
const createRes = await app.request("/api/bundles/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
specPath, rigRoot: sourceRoot,
bundleName: "author-bundle-test", bundleVersion: "0.1.0", outputPath,
}),
});
expect(createRes.status).toBe(201);
const createBody = await createRes.json();
expect(createBody.archiveHash).toMatch(/^[a-f0-9]{64}$/);
// 5. Unpack the archive + verify ALL 5 kinds' content + manifest fields
const unpackDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-author-bundle-unpack-"));
try {
const tar = await import("tar");
await tar.extract({ file: outputPath, cwd: unpackDir });
// Built bundle.yaml has all 5 cross-primitive fields
const builtYaml = fs.readFileSync(path.join(unpackDir, "bundle.yaml"), "utf-8");
expect(builtYaml).toContain("skills:");
expect(builtYaml).toContain("skills/author-skill/SKILL.md");
expect(builtYaml).toContain("plugins:");
expect(builtYaml).toContain("id: author-plugin");
expect(builtYaml).toContain("workflow_specs:");
expect(builtYaml).toContain("workflows/author-flow.yaml");
expect(builtYaml).toContain("context_packs:");
expect(builtYaml).toContain("context-packs/author-pack/manifest.yaml");
expect(builtYaml).toContain("agent_images:");
expect(builtYaml).toContain("agent-images/author-image");
// Vendored content actually landed in staging tree (now archive)
expect(fs.existsSync(path.join(unpackDir, "skills/author-skill/SKILL.md"))).toBe(true);
expect(fs.existsSync(path.join(unpackDir, "plugins/author-plugin/plugin.json"))).toBe(true);
expect(fs.existsSync(path.join(unpackDir, "plugins/author-plugin/README.md"))).toBe(true);
expect(fs.existsSync(path.join(unpackDir, "workflows/author-flow.yaml"))).toBe(true);
expect(fs.existsSync(path.join(unpackDir, "context-packs/author-pack/manifest.yaml"))).toBe(true);
expect(fs.existsSync(path.join(unpackDir, "context-packs/author-pack/brief.md"))).toBe(true);
expect(fs.existsSync(path.join(unpackDir, "agent-images/author-image/manifest.yaml"))).toBe(true);
} finally {
fs.rmSync(unpackDir, { recursive: true, force: true });
}
} finally {
fs.rmSync(sourceRoot, { recursive: true, force: true });
if (fs.existsSync(outputPath)) fs.rmSync(outputPath);
}
});
// Item 6 / Checkpoint 7.3e step 3: /install routes declared workflow_specs
// after successful bootstrap. Target = SettingsStore-resolved
// <workspaceSpecsRoot>/workflows. Bundle has workflow_specs[] with a path