Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ This project uses a Keep a Changelog-style structure. Until a tagged release is
- Additional WCAG requirement IDs used by `jp-public-web` are accepted by screening validation.
- Formal claim blockers are separated from auxiliary screening candidates.
- Public report visibility (`report --visibility public`) now withholds every target host that the Web network guard refuses, and records the reason in the redaction manifest. This adds IPv4-mapped, NAT64, 6to4 and site-local IPv6 forms, `192.88.99.0/24`, and `*.localhost` / `*.localdomain` names. Earlier builds kept some of these hosts; regenerate public reports made with them and complete publication review again.
- Registering or validating artifact paths that differ only in letter case is now rejected as a duplicate on macOS as well as Windows, because the default macOS filesystem is case-insensitive. Symlink, junction and containment checks are unchanged. A case-sensitive macOS volume can report a false duplicate; that is deliberate.

## Compatibility notes

Expand Down
1 change: 1 addition & 0 deletions release-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@
"tests/agent-role-contracts.test.mjs",
"tests/architecture-doc.test.mjs",
"tests/artifact-authoring.test.mjs",
"tests/artifact-path-identity.test.mjs",
"tests/assessment-output-contract.test.mjs",
"tests/assessment-review-provenance.test.mjs",
"tests/attestation-verifier.test.mjs",
Expand Down
4 changes: 2 additions & 2 deletions shared/skill/scripts/lib/audit-run-validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { validateParticipantBindings, collectParticipantEvidence } from "./parti
import { validateDeclaredChangeBindings, collectDeclaredChangeEvidence } from "./declared-change.mjs";
import { collectFixExecutionEvidence } from "./fix-execution-evidence.mjs";
import { targetBindingErrors } from "./run-targets.mjs";
import { pathKey, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs";
import { artifactPathKey, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs";
import { defaultSkillRoot, loadAuditResources } from "./audit-resources.mjs";
import { auditRunRegistryCompatibility, auditRunEnvelopeCompatibility } from "./orchestration-contracts.mjs";
import { artifactRootFor, validateArtifact, registeredArtifactPath, canonicalPermissions, remediationPermissionError, validateRegisteredArtifactEntries, validateArtifactEnvelopeSemantics, validateHumanQueueBindings, validateScreeningProfileBindings, validateDeclaredHumanBindings, validateRemediationBindings } from "./audit-artifact-validation.mjs";
Expand Down Expand Up @@ -158,7 +158,7 @@ export function validateAuditRun(run, { skillRoot = defaultSkillRoot, runFile, r
if (!artifactRoot) continue;
try {
const file = registeredArtifactPath(artifactRoot, entry);
const canonicalPath = pathKey(file);
const canonicalPath = artifactPathKey(file);
if (canonicalArtifactPaths.has(canonicalPath)) errors.push(`Duplicate canonical artifact path: ${String(entry?.path)}.`);
canonicalArtifactPaths.add(canonicalPath);
const snapshot = readArtifactFile(file, { label: `registered artifact ${String(entry?.artifact_id)}` });
Expand Down
4 changes: 2 additions & 2 deletions shared/skill/scripts/lib/audit-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { networkPolicyErrors } from "./network-policy.mjs";
import { afterChangeRun } from "./declared-change.mjs";
import { collectFixExecutionEvidence } from "./fix-execution-evidence.mjs";
import { targetInventoryErrors, targetBindingErrors, checkLocalRunTargets, consumeRunTargetCheck, checkRunTargets } from "./run-targets.mjs";
import { pathKey, isWithinPath, hasTraversal, inspectRealComponents, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs";
import { artifactPathKey, isWithinPath, hasTraversal, inspectRealComponents, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs";
import { defaultSkillRoot, loadAuditResources } from "./audit-resources.mjs";
import { compareText } from "./text-order.mjs";
import { roleFor, validateArtifact, registeredArtifactPath, canonicalPermissions, remediationPermissionError, validateFixHandoffBinding, validateChangeRecordAuthorizationBinding, assertCurrentOperationalRun } from "./audit-artifact-validation.mjs";
Expand Down Expand Up @@ -173,7 +173,7 @@ export function registerArtifact(run, artifact, options = {}) {
if (relativeSegments.includes(".fix-consumption") || relativeSegments.some((segment) => segment.includes("pending-change-record"))) {
throw new Error(`Internal fixer runtime evidence is not registerable as a completed artifact: ${relativePath}`);
}
if (run.artifacts.some((entry) => pathKey(registeredArtifactPath(validation.artifactRoot, entry)) === pathKey(artifactPath))) {
if (run.artifacts.some((entry) => artifactPathKey(registeredArtifactPath(validation.artifactRoot, entry)) === artifactPathKey(artifactPath))) {
throw new Error(`Duplicate artifact path: ${relativePath}`);
}
const snapshot = readStableFile(artifactPath, { label: "artifact file" });
Expand Down
5 changes: 5 additions & 0 deletions shared/skill/scripts/lib/safe-file-io.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export function pathKey(value) {
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
}

export function artifactPathKey(value, platform = process.platform) {
const normalized = path.normalize(path.resolve(value));
return platform === "win32" || platform === "darwin" ? normalized.toLowerCase() : normalized;
}

export function samePath(left, right) {
return pathKey(left) === pathKey(right);
}
Expand Down
31 changes: 31 additions & 0 deletions tests/artifact-path-identity.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import assert from "node:assert/strict";
import path from "node:path";
import test from "node:test";
import { artifactPathKey, pathKey, samePath } from "../codex/skills/information-accessibility-practice/scripts/lib/safe-file-io.mjs";

for (const platform of ["darwin", "win32", "linux"]) {
test(`artifact path identity ${platform === "linux" ? "preserves" : "folds"} case on ${platform}`, () => {
const upper = artifactPathKey("artifacts/Report.json", platform);
const lower = artifactPathKey("artifacts/report.json", platform);
if (platform === "linux") assert.notEqual(upper, lower);
else assert.equal(upper, lower);
assert.equal(upper, artifactPathKey(path.resolve("artifacts/Report.json"), platform));
});
}

test("artifact path identity defaults to the current platform", () => {
assert.equal(artifactPathKey("artifacts/Report.json"), artifactPathKey("artifacts/Report.json", process.platform));
});

test("path safety comparisons preserve case on darwin", () => {
const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
try {
Object.defineProperty(process, "platform", { value: "darwin" });
assert.notEqual(pathKey("artifacts/Report.json"), pathKey("artifacts/report.json"));
assert.equal(samePath("artifacts/Report.json", "artifacts/report.json"), false);
assert.equal(samePath("artifacts/Report.json", path.resolve("artifacts/Report.json")), true);
assert.equal(artifactPathKey("artifacts/Report.json"), artifactPathKey("artifacts/report.json"));
} finally {
Object.defineProperty(process, "platform", descriptor);
}
});
Loading