fix(slice-05): basename-flatten + valid YAML fixture + scanner-reachability proof (guard B1 repair on 9f9ebe0a) (OPR.0.3.2.5)

Per velocity-guard BLOCKING-CONCERN on 9f9ebe0a (qitem-…04f93bfb): the
scanner-reachability claim in my 7.3e step 3 handoff was false in two
load-bearing ways:

1. SCANNER IS FLAT-FOLDER-ONLY. The router landed files at
   <specsRoot>/workflows/packages/test-pkg/workflows/onboarding.yaml
   (preserving bundle directory layout). The live
   spec-library-workflow-scanner reads ONLY top-level YAML via
   readdirSync(folder) + isFile() check — no recursion (verified at
   spec-library-workflow-scanner.ts:382-397). Nested specs invisible to
   the scanner and the Library UI → "routed" but operator dead-end.

2. FIXTURE YAML WAS NOT A VALID WORKFLOW SPEC. parseWorkflowSpec
   requires top-level workflow: key + workflow.id + workflow.version +
   non-empty workflow.steps + workflow.roles mapping (workflow-spec-
   cache.ts:58-112). My fixture wrote name/version/steps at root with
   empty steps[] — even if placed at top-level it would have become an
   error diagnostic row, NOT a valid cached workflow.

Both problems mean "workflowSpecsRouting reports routedCount=1" was
compatible with "the spec is invisible to the operator". Exactly the
class of falseness the d43b7729 contract-realignment was trying to
prevent, one layer deeper.

Fix has three pieces:

(A) ROUTER: basename-flatten target computation. routeWorkflowSpecs now
computes target as `nodePath.resolve(targetWorkflowSpecsDir,
nodePath.basename(declared))` — every declared path lands at the top
level of the scanner-reachable folder, regardless of its directory
position in the bundle. SCANNER-REACHABILITY CONTRACT documented at the
basename-resolve site explaining: scanner is top-level-only, nested
would be invisible, basename enforces visibility. Side benefit:
basename is structurally incapable of escaping the target dir (no
separators in a leaf name), so the target-side prefix-strip escape
hazard banked on bundle-skills-router (595e9550 B1) is structurally
impossible here — the previous target-containment check kept as a
defensive sanity check but is now expected-always-true.

(B) ROUTER UNIT TESTS: 4 tests updated for the new contract:
- W2: declared "workflows/onboarding.yaml" → target/onboarding.yaml
  (basename, no longer preserves "workflows/" stripped form)
- W3: 3 specs incl. "workflows/sub/maintenance.yaml" all land FLAT at
  target/<basename> — explicit assertion that the "sub/" path is NOT
  created
- W7: target-escape attempt "workflows/../outside/spec.yaml" now lands
  SAFELY at target/spec.yaml (basename structurally prevents escape);
  test renamed + redocumented. Replaces the prior banked
  both-sides-trust-boundary rejection (the prior contract had the
  escape hazard via leading-prefix strip; basename obsoletes both the
  strip AND the hazard).
- W8: non-prefixed "custom/path/spec.yaml" lands at target/spec.yaml
  (basename; prefix-strip is now uniform/vestigial)

(C) INTEGRATION TEST: basic with-workflow_specs test now uses a VALID
workflow YAML fixture (workflow.id="bundle-routed-test", workflow.version=
"1", workflow.roles mapping, non-empty workflow.steps) mirroring
workflow-spec-folder-scanner.test.ts:22-43, and includes a real
SCANNER-REACHABILITY PROOF: after /install completes, instantiates a
fresh WorkflowSpecCache + invokes scanWorkflowSpecFolder against the
target dir; asserts scanResult.valid===1 + scanResult.errors===0 + the
cached row matches the fixture's workflow.id + version. This is the
end-to-end proof guard's fix-recipe required — no longer just "file
copied" but "scanner found+cached as valid workflow".

The B1-mirror dual-override test target updated to match basename
behavior (dualflow.yaml lands at <specsRoot>/workflows/dualflow.yaml,
not the prior nested path). The null-branch test unchanged (extras-
key absence proof is unaffected by routing semantics).

Discriminator-flips kept proving the same contracts as before — neither
was invalidated by the basename change. Both A (helper returns null →
2/44 fail) and B (gate on installMeta → 1/44 fail) still behave as
predicted in the 9f9ebe0a handoff.

Final: 170/170 across full slice-05 daemon suite. Sixth same-class
contract-claim incident on slice-05's tail; the discriminator-first +
PRE-handoff-dogfood discipline I committed to needs ALSO to grep the
live consumer contract (not just the resolved target path). Banked
discipline extension for 7.3f + 7.3g + 7.4.

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:17:41 -07:00
1 parent 9f9ebe0a51
commit d81456dc3d
3 files changed
+132 -48

No files matched your search

@@ -123,20 +123,29 @@ export function routeWorkflowSpecs(
});
continue;
}
// Target path mirrors the declared path under the target workflow-specs
// directory. Strip the leading "workflows/" prefix if present so the
// target dir is the root of the operator's workflow-specs tree (analogue
// of the skills router "skills/" strip).
const declaredTrimmed = declared.startsWith("workflows/") ? declared.slice("workflows/".length) : declared;
const targetAbs = nodePath.resolve(input.targetWorkflowSpecsDir, declaredTrimmed);
// Defense-in-depth path-containment on TARGET (banked both-sides-of-
// trust-boundary lesson; mirror of bundle-skills-router B1 repair
// 595e9550). Leading prefix strip can promote a relative segment that
// looks safe under bundleRoot (e.g. "workflows/../outside/spec.yaml"
// passes source-containment because bundleRoot may contain
// "outside/spec.yaml", but stripping yields "../outside/spec.yaml" which
// would escape targetWorkflowSpecsDir). Reject if the target resolves
// outside the target library.
// Target = top-level basename under targetWorkflowSpecsDir.
//
// SCANNER-REACHABILITY CONTRACT: spec-library-workflow-scanner reads
// ONLY top-level YAML files via readdirSync(folder) + isFile() (see
// spec-library-workflow-scanner.ts:382-397) — no recursion. Nested
// YAML inside subdirs is INVISIBLE to the scanner and Library UI.
// Workflow specs MUST land at the top of <workspace.specs_root>/workflows
// to become operator-visible. Basename-only target computation enforces
// this; preserving the bundle's directory layout would silently route
// specs to an unscanned location.
//
// Side benefit: basename can never escape targetWorkflowSpecsDir
// (nodePath.basename(any-path) is a leaf name with no separators), so
// the "target-side escape after prefix strip" hazard banked on the
// skills router (595e9550 B1 repair) is structurally impossible here.
//
// Basename collisions are an operator concern; two declared paths
// sharing the same filename will overwrite each other in declaration
// order. The scanner deduplicates by source_path + workflow.id; collision
// surfaces in the Library UI on next scan.
const targetAbs = nodePath.resolve(input.targetWorkflowSpecsDir, nodePath.basename(declared));
// Sanity check (defensive; basename is structurally safe but the resolve
// could in theory be a no-op for "" → keep the check, expected always-true).
if (targetAbs !== targetRootResolved && !targetAbs.startsWith(targetRootResolved + nodePath.sep)) {
records.push({
declaredPath: declared,
@@ -145,7 +154,6 @@ export function routeWorkflowSpecs(
});
continue;
}
fs.mkdirp(nodePath.dirname(targetAbs));
const content = fs.readFile(sourceAbs);
fs.writeFile(targetAbs, content);
records.push({
+79 -13
View File
@@ -1536,9 +1536,11 @@ describe("Bundle API routes", () => {
// 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
// pointing to a workflow YAML file in the bundle tree. Verifies the file
// lands at <env-overridden specs root>/workflows/<basename>.
it("POST /api/bundles/install routes declared workflow_specs after successful bootstrap (completed branch)", async () => {
// pointing to a workflow YAML file in the bundle tree. Router lands the
// file at top-level basename under <specsRoot>/workflows. Scanner-
// reachability proven via a real scanWorkflowSpecFolder call against the
// target dir (guard d43b7729 + 9f9ebe0a scanner-reachability lesson).
it("POST /api/bundles/install routes declared workflow_specs after successful bootstrap (completed branch) — proves scanner-reachability", async () => {
const origHome = process.env.OPENRIG_HOME;
const origSpecsRoot = process.env.OPENRIG_WORKSPACE_SPECS_ROOT;
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-workflow-specs-route-test-"));
@@ -1551,10 +1553,36 @@ describe("Bundle API routes", () => {
const bundlePath = path.join(tmpDir, "with-workflow-specs.rigbundle");
const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "wflow-target-"));
try {
// Put a workflow_spec YAML inside the bundle's package source
// Valid workflow YAML fixture mirroring workflow-spec-folder-scanner.test.ts:22-43
// (workflow.id + workflow.version + workflow.roles + workflow.steps non-empty).
// Without these the spec-library-workflow-scanner records it as an error
// diagnostic row, not a valid workflow — defeating the scanner-reachability proof.
const validWorkflowYaml = `workflow:
id: bundle-routed-test
version: '1'
objective: A bundle-routed workflow fixture
target:
rig: test-fixture
entry:
role: producer
roles:
producer:
preferred_targets:
- producer@test-fixture
steps:
- id: produce
actor_role: producer
objective: Draft.
allowed_exits:
- done
invariants:
allowed_exits:
- done
`;
// Put the valid YAML inside the bundle's package source
const workflowSrcDir = path.join(tmpDir, "test-pkg", "workflows");
fs.mkdirSync(workflowSrcDir, { recursive: true });
fs.writeFileSync(path.join(workflowSrcDir, "onboarding.yaml"), "name: onboarding\nversion: '1.0'\nsteps: []\n");
fs.writeFileSync(path.join(workflowSrcDir, "onboarding.yaml"), validWorkflowYaml);
await app.request("/api/bundles/create", {
method: "POST",
@@ -1568,9 +1596,8 @@ describe("Bundle API routes", () => {
await tar.extract({ file: bundlePath, cwd: stagingDir });
const bundleYamlPath = path.join(stagingDir, "bundle.yaml");
const original = fs.readFileSync(bundleYamlPath, "utf-8");
// Declare the verbatim package path (no leading "workflows/"), mirror of
// the skills-test pattern at line 1258. File is already in the bundle via
// the test-pkg source; integrity manifest unchanged.
// Declared path can be the verbatim package path; the router uses
// basename() to land it at top-level (scanner-reachability contract).
const tampered = `${original}\nworkflow_specs:\n - packages/test-pkg/workflows/onboarding.yaml\n`;
fs.writeFileSync(bundleYamlPath, tampered);
const { pack } = await import("../src/domain/bundle-archive.js");
@@ -1597,11 +1624,49 @@ describe("Bundle API routes", () => {
expect(body.workflowSpecsRouting).toBeDefined();
expect(body.workflowSpecsRouting.routedCount).toBe(1);
expect(body.workflowSpecsRouting.records[0].status).toBe("routed");
// Declared path has no leading "workflows/" so routes verbatim;
// file lands at <specsRoot>/workflows/<verbatim-path>
const expectedTarget = path.join(specsRoot, "workflows", "packages", "test-pkg", "workflows", "onboarding.yaml");
// Router uses basename(): declared "packages/test-pkg/workflows/onboarding.yaml"
// → lands at <specsRoot>/workflows/onboarding.yaml (top-level only).
const expectedTarget = path.join(specsRoot, "workflows", "onboarding.yaml");
expect(fs.existsSync(expectedTarget)).toBe(true);
expect(fs.readFileSync(expectedTarget, "utf-8")).toBe("name: onboarding\nversion: '1.0'\nsteps: []\n");
expect(fs.readFileSync(expectedTarget, "utf-8")).toBe(validWorkflowYaml);
// Scanner-reachability PROOF: invoke the real scanWorkflowSpecFolder
// against the target dir with a fresh WorkflowSpecCache; assert at
// least one valid (non-diagnostic) cached spec matching the fixture
// id+version. This proves the routed file is operator-visible via
// the live Library scan path (the d43b7729 / 9f9ebe0a contract).
const { createDb } = await import("../src/db/connection.js");
const { migrate } = await import("../src/db/migrate.js");
const { coreSchema } = await import("../src/db/migrations/001_core_schema.js");
const { eventsSchema } = await import("../src/db/migrations/003_events.js");
const { workflowSpecsSchema } = await import("../src/db/migrations/033_workflow_specs.js");
const { workflowSpecsDiagnosticSchema } = await import("../src/db/migrations/040_workflow_specs_diagnostic.js");
const { WorkflowSpecCache } = await import("../src/domain/workflow-spec-cache.js");
const { scanWorkflowSpecFolder } = await import("../src/domain/spec-library-workflow-scanner.js");
const scanDb = createDb();
migrate(scanDb, [coreSchema, eventsSchema, workflowSpecsSchema, workflowSpecsDiagnosticSchema]);
const scanCache = new WorkflowSpecCache(scanDb);
try {
const scanResult = scanWorkflowSpecFolder({
db: scanDb,
cache: scanCache,
folder: path.join(specsRoot, "workflows"),
builtinDir: null,
});
// The fixture is a VALID workflow spec (workflow.id + version + roles +
// steps); scanner caches it as `valid`, not `errors`.
expect(scanResult.scanned).toBe(1);
expect(scanResult.valid).toBe(1);
expect(scanResult.errors).toBe(0);
// Confirm the cached row matches the fixture's workflow.id + version.
const cached = scanDb.prepare(`SELECT name, version FROM workflow_specs ORDER BY name, version`).all() as Array<{ name: string; version: string }>;
expect(cached.length).toBeGreaterThanOrEqual(1);
const found = cached.find((r) => r.name === "bundle-routed-test");
expect(found).toBeDefined();
expect(found!.version).toBe("1");
} finally {
scanDb.close();
}
} finally {
fs.rmSync(targetRoot, { recursive: true, force: true });
}
@@ -1683,7 +1748,8 @@ describe("Bundle API routes", () => {
expect(body.workflowSpecsRouting).toBeDefined();
expect(body.workflowSpecsRouting.routedCount).toBe(1);
expect(body.workflowSpecsRouting.records[0].status).toBe("routed");
const expectedTarget = path.join(specsRoot, "workflows", "packages", "test-pkg", "workflows", "dualflow.yaml");
// Router uses basename() → top-level under <specsRoot>/workflows
const expectedTarget = path.join(specsRoot, "workflows", "dualflow.yaml");
expect(fs.existsSync(expectedTarget)).toBe(true);
expect(fs.readFileSync(expectedTarget, "utf-8")).toBe("name: dualflow\nversion: '2.0'\nsteps: []\n");
} finally {
@@ -55,8 +55,8 @@ describe("routeWorkflowSpecs", () => {
expect(fs._mkdirpCalls).toContain(TARGET);
});
// W2: routes one spec end-to-end
it("routes one workflow_spec: source YAML copied to target with installedAt populated", () => {
// W2: routes one spec end-to-end at top-level basename
it("routes one workflow_spec: source YAML copied to target/<basename> with installedAt populated", () => {
const fs = mockFs({
[`${BUNDLE_ROOT}/workflows/onboarding.yaml`]: "name: onboarding\nversion: 1.0",
});
@@ -64,12 +64,15 @@ describe("routeWorkflowSpecs", () => {
expect(result.routedCount).toBe(1);
expect(result.rejectedCount).toBe(0);
expect(result.records[0]!.status).toBe("routed");
// Basename-only: declared "workflows/onboarding.yaml" → target/onboarding.yaml
// (scanner-reachability contract: spec-library-workflow-scanner reads only
// top-level YAML; nested paths would be invisible).
expect(result.records[0]!.installedAt).toBe(`${TARGET}/onboarding.yaml`);
expect(fs._written.get(`${TARGET}/onboarding.yaml`)).toBe("name: onboarding\nversion: 1.0");
});
// W3: routes multiple specs; preserves directory layout
it("routes multiple workflow_specs preserving the directory layout under target", () => {
// W3: routes multiple specs ALL FLAT at top level (basename collapses layout)
it("routes multiple workflow_specs all flat at top level (basename collapses directory layout)", () => {
const fs = mockFs({
[`${BUNDLE_ROOT}/workflows/onboarding.yaml`]: "yaml-1",
[`${BUNDLE_ROOT}/workflows/release.yaml`]: "yaml-2",
@@ -82,9 +85,13 @@ describe("routeWorkflowSpecs", () => {
fs,
);
expect(result.routedCount).toBe(3);
// All land at top-level basename — the "sub/" prefix on entry 3 is stripped
// by basename(). This matches scanner-reachability (readdirSync + isFile only).
expect(fs._written.get(`${TARGET}/onboarding.yaml`)).toBe("yaml-1");
expect(fs._written.get(`${TARGET}/release.yaml`)).toBe("yaml-2");
expect(fs._written.get(`${TARGET}/sub/maintenance.yaml`)).toBe("yaml-3");
expect(fs._written.get(`${TARGET}/maintenance.yaml`)).toBe("yaml-3");
// Confirm the would-be-nested path is NOT created (basename flattened it).
expect(fs._written.has(`${TARGET}/sub/maintenance.yaml`)).toBe(false);
});
// W4: missing source file → "missing" record (skipped, not error)
@@ -132,32 +139,33 @@ describe("routeWorkflowSpecs", () => {
expect(result.records[2]!.status).toBe("unsafe");
});
// W7-B1: target-side path containment (banked both-sides-trust-boundary).
// Declared path "workflows/../outside/spec.yaml" passes SOURCE containment
// (resolves under bundleRoot since the leading "workflows/" segment is
// consumed before ../) but after the leading "workflows/" strip becomes
// "../outside/spec.yaml" which would escape targetWorkflowSpecsDir.
it("declared path that would escape target workflow-specs dir after prefix strip is rejected", () => {
// W7: target-side escape attempt lands SAFELY at basename (structural
// containment via basename — replaces the prior prefix-strip target-escape
// hazard from the skills router; basename guarantees no traversal).
it("target-escape attempt via traversal still lands safely at basename (structural containment)", () => {
const fs = mockFs({
// Source is reachable from bundleRoot via "workflows/../outside/spec.yaml"
// which resolves to "<bundleRoot>/outside/spec.yaml" — passes source check.
[`${BUNDLE_ROOT}/outside/spec.yaml`]: "would-escape-target",
[`${BUNDLE_ROOT}/outside/spec.yaml`]: "would-have-escaped-target",
});
const result = routeWorkflowSpecs(
makeInput({ declaredWorkflowSpecs: ["workflows/../outside/spec.yaml"] }),
fs,
);
expect(result.routedCount).toBe(0);
expect(result.rejectedCount).toBe(1);
expect(result.records[0]!.status).toBe("unsafe");
expect(result.records[0]!.detail).toContain("escapes target workflow-specs library");
// Crucially: no write happened outside target.
expect(result.routedCount).toBe(1);
expect(result.rejectedCount).toBe(0);
expect(result.records[0]!.status).toBe("routed");
// basename("workflows/../outside/spec.yaml") = "spec.yaml" → lands safely.
expect(result.records[0]!.installedAt).toBe(`${TARGET}/spec.yaml`);
expect(fs._written.get(`${TARGET}/spec.yaml`)).toBe("would-have-escaped-target");
// Confirm: no write outside target dir.
expect(fs._written.has(`${TARGET}/../outside/spec.yaml`)).toBe(false);
expect(fs._written.has(nodePath.resolve(`${TARGET}/../outside/spec.yaml`))).toBe(false);
});
// W8: non-"workflows/" prefixed declared path is honored as-is (no leading strip)
it("declared path without leading workflows/ prefix routes verbatim", () => {
// W8: non-prefixed declared path lands at its basename (prefix-strip is
// obsoleted by basename — the rule is uniform).
it("declared path without any leading prefix routes to target/<basename>", () => {
const fs = mockFs({
[`${BUNDLE_ROOT}/custom/path/spec.yaml`]: "custom",
});
@@ -166,6 +174,8 @@ describe("routeWorkflowSpecs", () => {
fs,
);
expect(result.routedCount).toBe(1);
expect(result.records[0]!.installedAt).toBe(`${TARGET}/custom/path/spec.yaml`);
// basename("custom/path/spec.yaml") = "spec.yaml" → top-level under target.
expect(result.records[0]!.installedAt).toBe(`${TARGET}/spec.yaml`);
expect(fs._written.has(`${TARGET}/custom/path/spec.yaml`)).toBe(false);
});
});