fix(slice-05): decouple skills routing from pre-check extraction (guard B1 repair on 2bf9c660) (OPR.0.3.2.5)

Repairs guard BLOCKING-CONCERN B1 on qitem-20260518220247-22f5257a /
2bf9c660: routeSkillsAfterBootstrap incorrectly depended on
installMeta which is only extracted when NOT skipVersionCheck OR NOT
force. When operator passes BOTH --skip-version-check AND --force
(the explicit override path for both pre-checks), installMeta stays
null and skills routing silently skipped even though the install
succeeded and the bundle declared skills.

This was wrong because skills routing is a POST-install side-effect
that has nothing to do with the version/conflict pre-check decisions.
The two override flags should disable their named pre-checks only,
not also disable an unrelated side-effect routing.

Fix applied (per guard's recipe):
- routes/bundles.ts: routeSkillsAfterBootstrap now takes bundlePath
  only and does its own safe unpack + parse internally (banked
  unpack trust boundary inherited). Decoupled completely from the
  pre-check extraction gate. Manifest re-parse is cheap vs. the
  unpack cost which is required either way to access the skill
  source files for routing.
- /install completed branch: simplified call to
  routeSkillsAfterBootstrap(bundlePath); no longer passes manifest
- bundle-routes.test.ts +1 test: install with skipVersionCheck=true
  AND force=true PLUS bundle declares skills, asserts
  skillsRouting.routedCount=1 + file exists at target. This is the
  exact regression guard's discriminator caught.

Verified:
- 38/38 daemon bundle-routes green (was 37; +1 new B1 repair test)
- Discriminator pre-verified: making routeSkillsAfterBootstrap return
  null causes 2/38 to fail (both with-skills tests — the original
  routes-skills test + the new dual-override test); restored returns
  38/38. Both positive tests now discriminate the routing function.
- bundle-skills-router 8/8 unchanged
- Coherence + audit-every-layer unchanged

Pre-commit + handoff-claim disciplines both applied. Diff: 2 files,
+86/-7 lines (only the helper signature change + removed call-site
manifest arg + new test).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-18 15:05:27 -07:00
1 parent 2bf9c6606f
commit 5f410eeef7
2 files changed
+101 -18

No files matched your search

+25 -18
View File
@@ -290,24 +290,33 @@ function skillsRouterFsOps(): SkillsRouterFsOps {
/**
* Item 6 / slice-05 Checkpoint 7.3: extract the bundle safely (banked unpack
* trust boundary) and route any declared skills to the operator skills
* library. Returns null when manifest has no skills[] (no-op). Called from
* /install completed branch — skills routing is independent of rig install
* state but only fires when install succeeded (partial/failed semantics
* deferred: keep skills routing tied to successful install outcome for v0).
* library. Returns null when bundle has no skills[] (no-op).
*
* B1 repair (qitem-20260518220247-22f5257a): takes bundlePath only and does
* its own safe unpack + parse. Previously coupled to installMeta from the
* pre-check extraction, which is null when operator passes --skip-version-
* check AND --force together; that incorrectly suppressed post-install
* skills routing on the dual-override path. Skills routing is independent
* of the pre-check decisions and should fire whenever an install completes
* successfully with a bundle that declares skills.
*
* Best-effort: returns null on any extract/parse failure (caller has the
* outer try/catch). Single unpack call per invocation; the manifest re-parse
* is cheap vs. the unpack cost which is required either way to access the
* skill source files for routing.
*/
async function routeSkillsAfterBootstrap(opts: {
bundlePath: string;
manifest: Record<string, unknown> | undefined;
}): Promise<RouteSkillsResult | null> {
if (!opts.manifest) return null;
const rawSkills = opts.manifest["skills"];
if (!Array.isArray(rawSkills) || rawSkills.length === 0) return null;
const declaredSkills = rawSkills.filter((s): s is string => typeof s === "string" && s.length > 0);
if (declaredSkills.length === 0) return null;
async function routeSkillsAfterBootstrap(bundlePath: string): Promise<RouteSkillsResult | null> {
const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-skills-route-"));
try {
await unpack(opts.bundlePath, tmpDir);
await unpack(bundlePath, tmpDir);
const manifestPath = nodePath.join(tmpDir, "bundle.yaml");
if (!fs.existsSync(manifestPath)) return null;
const manifestYaml = fs.readFileSync(manifestPath, "utf-8");
const manifest = parsePodBundleManifest(manifestYaml) as Record<string, unknown>;
const rawSkills = manifest["skills"];
if (!Array.isArray(rawSkills) || rawSkills.length === 0) return null;
const declaredSkills = rawSkills.filter((s): s is string => typeof s === "string" && s.length > 0);
if (declaredSkills.length === 0) return null;
return routeSkills(
{
bundleRoot: tmpDir,
@@ -752,9 +761,7 @@ bundleRoutes.post("/install", async (c) => {
// so operators see what landed.
let skillsRouting: RouteSkillsResult | null = null;
try {
skillsRouting = await routeSkillsAfterBootstrap({
bundlePath, manifest: installMeta?.bundleManifest,
});
skillsRouting = await routeSkillsAfterBootstrap(bundlePath);
} catch {
// Side-channel failure; install already succeeded
}
@@ -1299,6 +1299,82 @@ describe("Bundle API routes", () => {
}
});
// Item 6 / Checkpoint 7.3a / guard B1 repair (qitem-20260518220247): skills
// routing must fire even when operator uses BOTH --skip-version-check and
// --force pre-check overrides. The routing is independent of the pre-check
// extraction gate. Discriminator: re-coupling routeSkillsAfterBootstrap to
// installMeta would make this test fail (skillsRouting undefined when both
// override flags are set).
it("POST /api/bundles/install routes declared skills even when both --skip-version-check and --force are set (B1 repair)", async () => {
const origHome = process.env.OPENRIG_HOME;
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-skills-dual-override-"));
process.env.OPENRIG_HOME = auditHome;
const origBootstrap = setup.bootstrapOrchestrator.bootstrap.bind(setup.bootstrapOrchestrator);
try {
const { specPath } = seedPackage();
const bundlePath = path.join(tmpDir, "dual-override.rigbundle");
const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dual-override-target-"));
try {
const skillSourceDir = path.join(tmpDir, "test-pkg", "skills");
fs.mkdirSync(skillSourceDir, { recursive: true });
fs.writeFileSync(path.join(skillSourceDir, "DUAL.md"), "# dual-override skill body");
await app.request("/api/bundles/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ specPath, bundleName: "dual-override", bundleVersion: "0.1.0", outputPath: bundlePath }),
});
const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), "dual-override-stage-"));
try {
const tar = await import("tar");
await tar.extract({ file: bundlePath, cwd: stagingDir });
const bundleYamlPath = path.join(stagingDir, "bundle.yaml");
const original = fs.readFileSync(bundleYamlPath, "utf-8");
const tampered = `${original}\nskills:\n - packages/test-pkg/skills/DUAL.md\n`;
fs.writeFileSync(bundleYamlPath, tampered);
const { pack } = await import("../src/domain/bundle-archive.js");
await pack(stagingDir, bundlePath);
} finally {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
const stub = vi.fn().mockResolvedValue({
status: "completed",
runId: "test-run-dual-override",
rigId: "01H000000000000000DUAL0001",
stages: [{ stage: "resolve_spec", status: "ok" }],
errors: [],
});
(setup.bootstrapOrchestrator as unknown as { bootstrap: typeof stub }).bootstrap = stub;
// Critical: both override flags set — this triggered the B1 bug
const installRes = await app.request("/api/bundles/install", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
bundlePath, targetRoot, autoApprove: true,
skipVersionCheck: true, force: true,
}),
});
const body = await installRes.json();
expect(body.skillsRouting).toBeDefined();
expect(body.skillsRouting.routedCount).toBe(1);
expect(body.skillsRouting.records[0].status).toBe("routed");
const expectedTarget = path.join(auditHome, "skills", "packages", "test-pkg", "skills", "DUAL.md");
expect(fs.existsSync(expectedTarget)).toBe(true);
expect(fs.readFileSync(expectedTarget, "utf-8")).toBe("# dual-override skill body");
} finally {
fs.rmSync(targetRoot, { recursive: true, force: true });
}
} finally {
(setup.bootstrapOrchestrator as unknown as { bootstrap: typeof origBootstrap }).bootstrap = origBootstrap;
if (origHome === undefined) delete process.env.OPENRIG_HOME;
else process.env.OPENRIG_HOME = origHome;
fs.rmSync(auditHome, { recursive: true, force: true });
}
});
it("POST /api/bundles/install does NOT include skillsRouting when bundle has no skills[]", async () => {
const origHome = process.env.OPENRIG_HOME;
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-skills-no-test-"));