From 3d709f1c7a3b3c0426a800df5231330815c66ef1 Mon Sep 17 00:00:00 2001 From: openboa Date: Wed, 9 Sep 2026 18:32:08 +0900 Subject: [PATCH 1/3] ci: adopt isolated central controls and repository-owned verification --- .github/ci-policy.mjs | 364 ---------------------------------- .github/merge-policy.json | 20 +- .github/verify.mjs | 115 +++++++++++ .github/verify.test.mjs | 38 ++++ .github/workflows/trusted.yml | 4 +- CODEOWNERS | 32 +-- README.md | 12 ++ package.json | 2 +- 8 files changed, 199 insertions(+), 388 deletions(-) delete mode 100644 .github/ci-policy.mjs create mode 100644 .github/verify.mjs create mode 100644 .github/verify.test.mjs diff --git a/.github/ci-policy.mjs b/.github/ci-policy.mjs deleted file mode 100644 index 735c1ed..0000000 --- a/.github/ci-policy.mjs +++ /dev/null @@ -1,364 +0,0 @@ -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { resolve } from "node:path"; - -const root = resolve(process.env.CI_POLICY_ROOT ?? "."); -const TRUSTED_CONTROL_SHA = "f33da6bbcdfebd0693ff7673d750f369629e000e"; -const readJson = (path) => JSON.parse(readFileSync(resolve(root, path), "utf8")); -const trackedFiles = execFileSync("git", ["-C", root, "ls-files", "-z"], { - encoding: "utf8", -}) - .split("\0") - .filter(Boolean); -function trackedEntries(directory = ".") { - const prefix = directory === "." ? "" : `${directory.replace(/\/$/u, "")}/`; - const entries = new Set(); - for (const file of trackedFiles) { - if (!file.startsWith(prefix)) continue; - const remainder = file.slice(prefix.length); - if (!remainder) continue; - entries.add(remainder.split("/")[0]); - } - return [...entries].sort(); -} -function checkoutEntries(directory = ".") { - const entries = trackedEntries(directory); - if (directory === ".") entries.push(".git"); - return entries.sort(); -} -assert.equal(existsSync(resolve(root, ".npmrc")), false); -assert.equal(existsSync(resolve(root, "npm-shrinkwrap.json")), false); -assert.deepEqual( - readdirSync(resolve(root, ".github/workflows")).sort(), - ["trusted.yml"], -); -assert.equal( - readFileSync(resolve(root, ".github/workflows/trusted.yml"), "utf8"), - `name: OpenBoa Coffee trusted gate - -on: - pull_request_target: - types: [opened, synchronize, reopened, ready_for_review] - -permissions: {} - -jobs: - trusted: - name: OpenBoa Coffee trusted required - permissions: - actions: read - contents: read - security-events: write - uses: openboa-ai/.github/.github/workflows/coffee-trusted-gate.yml@${TRUSTED_CONTROL_SHA} - with: - control_sha: ${TRUSTED_CONTROL_SHA} -`, - "trusted wrapper must remain exact", -); -assert.deepEqual(checkoutEntries(), [ - ".git", - ".gitattributes", - ".githooks", - ".github", - ".gitignore", - "AGENTS.md", - "CODEOWNERS", - "LICENSE", - "README.md", - "SECURITY.md", - "evals", - "graders", - "package-lock.json", - "package.json", - "research", -]); - -assert.deepEqual(readJson("package.json"), { - name: "@openboa-ai/coffee-chat-bench", - version: "0.0.0", - private: true, - type: "module", - scripts: { - "hooks:install": "git config core.hooksPath .githooks", - verify: "node .github/ci-policy.mjs", - }, -}); -assert.deepEqual(readJson("package-lock.json"), { - name: "@openboa-ai/coffee-chat-bench", - version: "0.0.0", - lockfileVersion: 3, - requires: true, - packages: { - "": { - name: "@openboa-ai/coffee-chat-bench", - version: "0.0.0", - }, - }, -}); -assert.deepEqual(readdirSync(resolve(root, ".github")).sort(), [ - "PULL_REQUEST_TEMPLATE.md", - "ci-policy.mjs", - "dependabot.yml", - "merge-policy.json", - "workflows", -]); -assert.deepEqual(readdirSync(resolve(root, ".githooks")).sort(), ["pre-commit"]); -const expectedHook = [ - "#!/bin/sh", - "set -eu", - "", - "scanner=${GITLEAKS_BIN:-gitleaks}", - 'if ! command -v "$scanner" >/dev/null 2>&1; then', - " printf '%s\\n' 'Gitleaks is required; install Gitleaks before committing.' >&2", - " exit 1", - "fi", - "", - "if [ -e .gitleaks.toml ] || [ -e .gitleaksignore ]; then", - " printf '%s\\n' 'Repository-local Gitleaks controls are not permitted.' >&2", - " exit 1", - "fi", - "unset GITLEAKS_CONFIG GITLEAKS_CONFIG_TOML", - '"$scanner" git --pre-commit --staged --gitleaks-ignore-path /dev/null \\', - " --ignore-gitleaks-allow --redact --no-banner .", - 'staged_dir="$(mktemp -d)"', - `trap 'rm -rf "$staged_dir"' EXIT HUP INT TERM`, - 'git checkout-index --all --prefix="$staged_dir/"', - '"$scanner" dir --gitleaks-ignore-path /dev/null --ignore-gitleaks-allow \\', - ' --redact --no-banner "$staged_dir"', - "", -].join("\n"); -assert.equal(readFileSync(resolve(root, ".githooks/pre-commit"), "utf8"), expectedHook); -assert.notEqual(statSync(resolve(root, ".githooks/pre-commit")).mode & 0o111, 0); -assert.equal( - readFileSync(resolve(root, ".gitignore"), "utf8"), - `# Local credentials -/.superpowers/ -.env -.env.* -!.env.example -credentials.json -secrets.json -*.private.pem -private-key.pem -*.private.key -private.key -private-key.key -id_rsa -id_dsa -id_ecdsa -id_ed25519 -tls.key -server.key -server-key.pem -*-private-key.pem -*-private-key.key -privkey*.pem -*.p12 -*.pfx -*.jks -node_modules/ -coverage/ -dist/ -*.tsbuildinfo -__pycache__/ -*.pyc -`, - ".gitignore must preserve the credential and local-artifact ignore contract", -); - -assert.equal( - readFileSync(resolve(root, "CODEOWNERS"), "utf8"), - `# No wildcard owner: ordinary paths may become eligible for strong-CI auto-merge. -/.github/** @openboa-ai/security-maintainers -/.githooks/** @openboa-ai/security-maintainers -/.gitleaks* @openboa-ai/security-maintainers -/AGENTS.md @openboa-ai/security-maintainers -/CODEOWNERS @openboa-ai/security-maintainers -/LICENSE @openboa -/README.md @openboa -/.npmrc @openboa-ai/security-maintainers -/npm-shrinkwrap.json @openboa-ai/security-maintainers -/package.json @openboa-ai/security-maintainers -/package-lock.json @openboa-ai/security-maintainers -/prettier.config.mjs @openboa-ai/security-maintainers -/SECURITY.md @openboa-ai/security-maintainers -/evals/** @openboa-ai/security-maintainers -/graders/** @openboa-ai/security-maintainers -/research/** @openboa-ai/security-maintainers -`, - "CODEOWNERS must preserve the benchmark ownership routes", -); -assert.match( - readFileSync(resolve(root, "SECURITY.md"), "utf8"), - /security@openboa\.ai/u, - "SECURITY.md must provide a private reporting channel", -); - -assert.deepEqual(readJson(".github/merge-policy.json"), { - repository_role: "bench", - merge_method: "squash", - auto_merge: "github-native", - merge_queue: false, - required_events: ["pull_request"], - eligible_author_associations: ["OWNER", "MEMBER"], - eligible_bot_logins: ["dependabot[bot]"], - required_approvals: 0, - required_checks: [ - { - context: "OpenBoa Coffee trusted required / OpenBoa Coffee trusted required", - integration_id: 15368, - }, - ], - sensitive_review: { - enforcement: "github_environment", - environment: "coffee-security", - required_approvals: 1, - prevent_self_review: false, - }, - protected_paths: [ - "/.github/**", - "/.githooks/**", - "/.gitleaksignore", - "/.gitleaks.toml", - "/AGENTS.md", - "/CODEOWNERS", - "/README.md", - "/SECURITY.md", - "/.npmrc", - "/npm-shrinkwrap.json", - "/package.json", - "/package-lock.json", - "/prettier.config.mjs", - "/evals/**", - "/graders/**", - "/research/**", - ], -}); - -const expectedFiles = [ - "README.md", - "evals/README.md", - "graders/README.md", - "research/README.md", -]; -for (const file of expectedFiles) { - assert.equal(existsSync(resolve(root, file)), true, file); -} - -const expectedDirectoryEntries = new Map([ - ["evals", ["README.md", "output-quality", "triggering"]], - ["evals/output-quality", ["perspective-application", "perspective-capture"]], - [ - "evals/output-quality/perspective-application", - ["agent-judgment-action", "human-understanding"], - ], - ["evals/output-quality/perspective-capture", [".gitkeep"]], - [ - "evals/output-quality/perspective-application/human-understanding", - [".gitkeep"], - ], - [ - "evals/output-quality/perspective-application/agent-judgment-action", - [".gitkeep"], - ], - ["evals/triggering", ["perspective-application", "perspective-capture"]], - ["evals/triggering/perspective-capture", [".gitkeep"]], - ["evals/triggering/perspective-application", [".gitkeep"]], -]); -for (const [directory, entries] of expectedDirectoryEntries) { - assert.deepEqual(trackedEntries(directory), entries, directory); - for (const entry of entries) { - if (entry === ".gitkeep") { - const placeholderPath = resolve(root, directory, entry); - const placeholder = lstatSync(placeholderPath); - assert.equal(placeholder.isSymbolicLink(), false, `${directory}/${entry} must not be a symlink`); - assert.equal(placeholder.isFile(), true, `${directory}/${entry} must be a regular file`); - assert.equal( - readFileSync(placeholderPath, "utf8"), - "", - `${directory}/${entry} must remain empty`, - ); - } - } -} -for (const directory of ["graders", "research"]) { - assert.deepEqual(trackedEntries(directory), ["README.md"], directory); -} - -assert.equal( - readFileSync(resolve(root, ".github/dependabot.yml"), "utf8"), - `version: 2 - -updates: - - package-ecosystem: npm - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 - commit-message: - prefix: deps - allow: - - dependency-name: "*" - update-types: - - version-update:semver-minor - - version-update:semver-patch - groups: - security: - applies-to: security-updates - patterns: - - "*" - production: - applies-to: version-updates - dependency-type: production - update-types: [minor, patch] - development: - applies-to: version-updates - dependency-type: development - update-types: [minor, patch] - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 - commit-message: - prefix: deps - allow: - - dependency-name: "*" - update-types: - - version-update:semver-minor - - version-update:semver-patch - groups: - security: - applies-to: security-updates - patterns: - - "*" - versions: - applies-to: version-updates - update-types: [minor, patch] - patterns: - - "*" -`, - "Dependabot policy must remain bounded to approved update lanes", -); - -const forbidden = [ - "bank", - "harbor", - "qualification", - "schemas", - "src", - "tests", - "scripts", -]; -for (const directory of forbidden) { - assert.equal(existsSync(resolve(root, directory)), false, directory); -} - -const readme = readFileSync(resolve(root, "README.md"), "utf8"); -assert.match(readme, /Ground Truth/u); -assert.match(readme, /Each future case uses the same envelope:/u); -for (const marker of ["prompt/", "input/", "expected-output/"]) { - assert.match(readme, new RegExp(`\\b${marker.replace("/", "\\/")}`, "u"), marker); -} -console.log("Coffee Chat Bench structure and policy passed."); diff --git a/.github/merge-policy.json b/.github/merge-policy.json index 3a0b3ff..22cdda6 100644 --- a/.github/merge-policy.json +++ b/.github/merge-policy.json @@ -3,9 +3,16 @@ "merge_method": "squash", "auto_merge": "github-native", "merge_queue": false, - "required_events": ["pull_request"], - "eligible_author_associations": ["OWNER", "MEMBER"], - "eligible_bot_logins": ["dependabot[bot]"], + "required_events": [ + "pull_request" + ], + "eligible_author_associations": [ + "OWNER", + "MEMBER" + ], + "eligible_bot_logins": [ + "dependabot[bot]" + ], "required_approvals": 0, "required_checks": [ { @@ -35,6 +42,9 @@ "/prettier.config.mjs", "/evals/**", "/graders/**", - "/research/**" - ] + "/research/**", + "/.gitignore", + "/.gitattributes" + ], + "required_code_owner_reviews": 1 } diff --git a/.github/verify.mjs b/.github/verify.mjs new file mode 100644 index 0000000..af1af73 --- /dev/null +++ b/.github/verify.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { existsSync, lstatSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const root = resolve(process.env.CI_POLICY_ROOT ?? "."); +const trackedFiles = execFileSync("git", ["-C", root, "ls-files", "-z"], { + encoding: "utf8", +}) + .split("\0") + .filter(Boolean); +function trackedEntries(directory = ".") { + const prefix = directory === "." ? "" : `${directory.replace(/\/$/u, "")}/`; + const entries = new Set(); + for (const file of trackedFiles) { + if (!file.startsWith(prefix)) continue; + const remainder = file.slice(prefix.length); + if (!remainder) continue; + entries.add(remainder.split("/")[0]); + } + return [...entries].sort(); +} +function checkoutEntries(directory = ".") { + const entries = trackedEntries(directory); + if (directory === ".") entries.push(".git"); + return entries.sort(); +} +assert.deepEqual(checkoutEntries(), [ + ".git", + ".gitattributes", + ".githooks", + ".github", + ".gitignore", + "AGENTS.md", + "CODEOWNERS", + "LICENSE", + "README.md", + "SECURITY.md", + "evals", + "graders", + "package-lock.json", + "package.json", + "research", +]); + +const expectedFiles = [ + "README.md", + "evals/README.md", + "graders/README.md", + "research/README.md", +]; +for (const file of expectedFiles) { + assert.equal(existsSync(resolve(root, file)), true, file); +} + +const expectedDirectoryEntries = new Map([ + ["evals", ["README.md", "output-quality", "triggering"]], + ["evals/output-quality", ["perspective-application", "perspective-capture"]], + [ + "evals/output-quality/perspective-application", + ["agent-judgment-action", "human-understanding"], + ], + ["evals/output-quality/perspective-capture", [".gitkeep"]], + [ + "evals/output-quality/perspective-application/human-understanding", + [".gitkeep"], + ], + [ + "evals/output-quality/perspective-application/agent-judgment-action", + [".gitkeep"], + ], + ["evals/triggering", ["perspective-application", "perspective-capture"]], + ["evals/triggering/perspective-capture", [".gitkeep"]], + ["evals/triggering/perspective-application", [".gitkeep"]], +]); +for (const [directory, entries] of expectedDirectoryEntries) { + assert.deepEqual(trackedEntries(directory), entries, directory); + for (const entry of entries) { + if (entry === ".gitkeep") { + const placeholderPath = resolve(root, directory, entry); + const placeholder = lstatSync(placeholderPath); + assert.equal(placeholder.isSymbolicLink(), false, `${directory}/${entry} must not be a symlink`); + assert.equal(placeholder.isFile(), true, `${directory}/${entry} must be a regular file`); + assert.equal( + readFileSync(placeholderPath, "utf8"), + "", + `${directory}/${entry} must remain empty`, + ); + } + } +} +for (const directory of ["graders", "research"]) { + assert.deepEqual(trackedEntries(directory), ["README.md"], directory); +} + +const forbidden = [ + "bank", + "harbor", + "qualification", + "schemas", + "src", + "tests", + "scripts", +]; +for (const directory of forbidden) { + assert.equal(existsSync(resolve(root, directory)), false, directory); +} + +const readme = readFileSync(resolve(root, "README.md"), "utf8"); +assert.match(readme, /Ground Truth/u); +assert.match(readme, /Each future case uses the same envelope:/u); +for (const marker of ["prompt/", "input/", "expected-output/"]) { + assert.match(readme, new RegExp(`\\b${marker.replace("/", "\\/")}`, "u"), marker); +} +console.log("Coffee Chat Bench structure verification passed."); diff --git a/.github/verify.test.mjs b/.github/verify.test.mjs new file mode 100644 index 0000000..e28aadb --- /dev/null +++ b/.github/verify.test.mjs @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; + +const source = resolve(import.meta.dirname, ".."); +function fixture(mutate = () => {}) { + const root = mkdtempSync(join(tmpdir(), "coffee-chat-bench-verify-")); + const env = { ...process.env, CI_POLICY_ROOT: root }; + delete env.GIT_INDEX_FILE; + try { + const files = execFileSync("git", ["ls-files", "-z"], { cwd: source, encoding: "utf8", env }).split("\0").filter(Boolean); + for (const path of files) { + mkdirSync(dirname(join(root, path)), { recursive: true }); + cpSync(join(source, path), join(root, path)); + } + mutate(root); + execFileSync("git", ["init", "-q"], { cwd: root, env }); + execFileSync("git", ["add", "-f", "--all"], { cwd: root, env }); + return spawnSync(process.execPath, [join(source, ".github/verify.mjs")], { cwd: root, env, encoding: "utf8" }); + } finally { rmSync(root, { recursive: true, force: true }); } +} + +test("accepts the current repository contract", () => { + const result = fixture(); + assert.equal(result.status, 0, result.stderr); +}); + +test("rejects unpublished cases in the empty public benchmark", () => { + const result = fixture((root) => writeFileSync(join(root, "evals/evals.json"), "[]")); + assert.notEqual(result.status, 0, result.stdout); +}); +test("rejects nonempty placeholders", () => { + const result = fixture((root) => writeFileSync(join(root, "evals/output-quality/perspective-capture/.gitkeep"), "unreviewed data")); + assert.notEqual(result.status, 0, result.stdout); +}); diff --git a/.github/workflows/trusted.yml b/.github/workflows/trusted.yml index b63f0c6..1e507cf 100644 --- a/.github/workflows/trusted.yml +++ b/.github/workflows/trusted.yml @@ -13,6 +13,6 @@ jobs: actions: read contents: read security-events: write - uses: openboa-ai/.github/.github/workflows/coffee-trusted-gate.yml@f33da6bbcdfebd0693ff7673d750f369629e000e + uses: openboa-ai/.github/.github/workflows/coffee-trusted-gate.yml@5b4d641ad1951c529d0a88f08df2544d25c3811a with: - control_sha: f33da6bbcdfebd0693ff7673d750f369629e000e + control_sha: 5b4d641ad1951c529d0a88f08df2544d25c3811a diff --git a/CODEOWNERS b/CODEOWNERS index a999381..ed69147 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,17 +1,17 @@ # No wildcard owner: ordinary paths may become eligible for strong-CI auto-merge. -/.github/** @openboa-ai/security-maintainers -/.githooks/** @openboa-ai/security-maintainers -/.gitleaks* @openboa-ai/security-maintainers -/AGENTS.md @openboa-ai/security-maintainers -/CODEOWNERS @openboa-ai/security-maintainers -/LICENSE @openboa -/README.md @openboa -/.npmrc @openboa-ai/security-maintainers -/npm-shrinkwrap.json @openboa-ai/security-maintainers -/package.json @openboa-ai/security-maintainers -/package-lock.json @openboa-ai/security-maintainers -/prettier.config.mjs @openboa-ai/security-maintainers -/SECURITY.md @openboa-ai/security-maintainers -/evals/** @openboa-ai/security-maintainers -/graders/** @openboa-ai/security-maintainers -/research/** @openboa-ai/security-maintainers +/.github/** @openboa-ai/security-maintainers @SonSangjoon +/.githooks/** @openboa-ai/security-maintainers @SonSangjoon +/.gitleaks* @openboa-ai/security-maintainers @SonSangjoon +/AGENTS.md @openboa-ai/security-maintainers @SonSangjoon +/CODEOWNERS @openboa-ai/security-maintainers @SonSangjoon +/LICENSE @openboa @SonSangjoon +/README.md @openboa @SonSangjoon +/.npmrc @openboa-ai/security-maintainers @SonSangjoon +/npm-shrinkwrap.json @openboa-ai/security-maintainers @SonSangjoon +/package.json @openboa-ai/security-maintainers @SonSangjoon +/package-lock.json @openboa-ai/security-maintainers @SonSangjoon +/prettier.config.mjs @openboa-ai/security-maintainers @SonSangjoon +/SECURITY.md @openboa-ai/security-maintainers @SonSangjoon +/evals/** @openboa-ai/security-maintainers @SonSangjoon +/graders/** @openboa-ai/security-maintainers @SonSangjoon +/research/** @openboa-ai/security-maintainers @SonSangjoon diff --git a/README.md b/README.md index f6f03b0..c10336e 100644 --- a/README.md +++ b/README.md @@ -177,3 +177,15 @@ calibrated, and coffee-chat-eval preserves reproducible execution evidence. Benchmark definitions and documentation are MIT licensed, Copyright © 2026 Openboa AI. Any future case material must carry its own verified rights and provenance. + +## Repository verification + +Run `npm ci --ignore-scripts --no-bin-links`, then `npm run verify`. +The checks and regressions in `.github/verify.mjs` and `.github/verify.test.mjs` +are owned here. The pinned central workflow owns security policy, execution +isolation and approval; it does not define this repository's product or data +layout. Existing CODEOWNERS routes and protected paths remain in force. + +The same verify command runs locally and in the isolated CI lane. A changed +central pin needs control review; never remove a required check to upgrade it. +Structural CI success does not establish benchmark validity or Product lift. diff --git a/package.json b/package.json index bb0784e..df56a5c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,6 @@ "type": "module", "scripts": { "hooks:install": "git config core.hooksPath .githooks", - "verify": "node .github/ci-policy.mjs" + "verify": "node --test .github/verify.test.mjs && node .github/verify.mjs" } } From ea3dc66a8138e9c55377f8642543c3ce34b46408 Mon Sep 17 00:00:00 2001 From: openboa Date: Wed, 9 Sep 2026 18:45:12 +0900 Subject: [PATCH 2/3] fix: preserve infrastructure directory boundaries --- .github/verify.mjs | 16 ++++++++++++++++ .github/verify.test.mjs | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/.github/verify.mjs b/.github/verify.mjs index af1af73..cd53c3b 100644 --- a/.github/verify.mjs +++ b/.github/verify.mjs @@ -9,6 +9,22 @@ const trackedFiles = execFileSync("git", ["-C", root, "ls-files", "-z"], { }) .split("\0") .filter(Boolean); +// Infrastructure directories are not an escape hatch for Product/data artifacts. +// Central controls validate security semantics; this repository owns its layout. +assert.deepEqual( + trackedFiles.filter((path) => path.startsWith(".github/") || path.startsWith(".githooks/")).sort(), + [ + ".githooks/pre-commit", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/dependabot.yml", + ".github/merge-policy.json", + ".github/verify.mjs", + ".github/verify.test.mjs", + ".github/workflows/trusted.yml", + ], + "unexpected or missing infrastructure file", +); + function trackedEntries(directory = ".") { const prefix = directory === "." ? "" : `${directory.replace(/\/$/u, "")}/`; const entries = new Set(); diff --git a/.github/verify.test.mjs b/.github/verify.test.mjs index e28aadb..527e5cd 100644 --- a/.github/verify.test.mjs +++ b/.github/verify.test.mjs @@ -23,6 +23,13 @@ function fixture(mutate = () => {}) { } finally { rmSync(root, { recursive: true, force: true }); } } +for (const path of [".github/product-behavior.js", ".githooks/eval-results.json"]) { + test(`rejects unexpected infrastructure artifact: ${path}`, () => { + const result = fixture((root) => writeFileSync(join(root, path), "{}")); + assert.notEqual(result.status, 0, result.stdout); + }); +} + test("accepts the current repository contract", () => { const result = fixture(); assert.equal(result.status, 0, result.stderr); From f4c295fa75cee6b98dcaa748b787cbe4c51a5297 Mon Sep 17 00:00:00 2001 From: openboa Date: Wed, 9 Sep 2026 19:06:15 +0900 Subject: [PATCH 3/3] fix: validate complete repository file paths --- .github/verify.mjs | 110 +++++++++------------------------------- .github/verify.test.mjs | 11 ++++ 2 files changed, 34 insertions(+), 87 deletions(-) diff --git a/.github/verify.mjs b/.github/verify.mjs index cd53c3b..6558e95 100644 --- a/.github/verify.mjs +++ b/.github/verify.mjs @@ -9,104 +9,40 @@ const trackedFiles = execFileSync("git", ["-C", root, "ls-files", "-z"], { }) .split("\0") .filter(Boolean); -// Infrastructure directories are not an escape hatch for Product/data artifacts. -// Central controls validate security semantics; this repository owns its layout. -assert.deepEqual( - trackedFiles.filter((path) => path.startsWith(".github/") || path.startsWith(".githooks/")).sort(), - [ - ".githooks/pre-commit", - ".github/PULL_REQUEST_TEMPLATE.md", - ".github/dependabot.yml", - ".github/merge-policy.json", - ".github/verify.mjs", - ".github/verify.test.mjs", - ".github/workflows/trusted.yml", - ], - "unexpected or missing infrastructure file", -); - -function trackedEntries(directory = ".") { - const prefix = directory === "." ? "" : `${directory.replace(/\/$/u, "")}/`; - const entries = new Set(); - for (const file of trackedFiles) { - if (!file.startsWith(prefix)) continue; - const remainder = file.slice(prefix.length); - if (!remainder) continue; - entries.add(remainder.split("/")[0]); - } - return [...entries].sort(); -} -function checkoutEntries(directory = ".") { - const entries = trackedEntries(directory); - if (directory === ".") entries.push(".git"); - return entries.sort(); -} -assert.deepEqual(checkoutEntries(), [ - ".git", +// Compare complete paths: a same-named directory is not an allowed file. +// This is the published repository layout, not central security policy. +assert.deepEqual(trackedFiles.slice().sort(), [ ".gitattributes", - ".githooks", - ".github", + ".githooks/pre-commit", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/dependabot.yml", + ".github/merge-policy.json", + ".github/verify.mjs", + ".github/verify.test.mjs", + ".github/workflows/trusted.yml", ".gitignore", "AGENTS.md", "CODEOWNERS", "LICENSE", "README.md", "SECURITY.md", - "evals", - "graders", - "package-lock.json", - "package.json", - "research", -]); - -const expectedFiles = [ - "README.md", "evals/README.md", + "evals/output-quality/perspective-application/agent-judgment-action/.gitkeep", + "evals/output-quality/perspective-application/human-understanding/.gitkeep", + "evals/output-quality/perspective-capture/.gitkeep", + "evals/triggering/perspective-application/.gitkeep", + "evals/triggering/perspective-capture/.gitkeep", "graders/README.md", - "research/README.md", -]; -for (const file of expectedFiles) { - assert.equal(existsSync(resolve(root, file)), true, file); + "package-lock.json", + "package.json", + "research/README.md" +], "unexpected or missing repository file"); +for (const path of trackedFiles) { + assert.equal(lstatSync(resolve(root, path)).isFile(), true, `${path}: regular file required`); } -const expectedDirectoryEntries = new Map([ - ["evals", ["README.md", "output-quality", "triggering"]], - ["evals/output-quality", ["perspective-application", "perspective-capture"]], - [ - "evals/output-quality/perspective-application", - ["agent-judgment-action", "human-understanding"], - ], - ["evals/output-quality/perspective-capture", [".gitkeep"]], - [ - "evals/output-quality/perspective-application/human-understanding", - [".gitkeep"], - ], - [ - "evals/output-quality/perspective-application/agent-judgment-action", - [".gitkeep"], - ], - ["evals/triggering", ["perspective-application", "perspective-capture"]], - ["evals/triggering/perspective-capture", [".gitkeep"]], - ["evals/triggering/perspective-application", [".gitkeep"]], -]); -for (const [directory, entries] of expectedDirectoryEntries) { - assert.deepEqual(trackedEntries(directory), entries, directory); - for (const entry of entries) { - if (entry === ".gitkeep") { - const placeholderPath = resolve(root, directory, entry); - const placeholder = lstatSync(placeholderPath); - assert.equal(placeholder.isSymbolicLink(), false, `${directory}/${entry} must not be a symlink`); - assert.equal(placeholder.isFile(), true, `${directory}/${entry} must be a regular file`); - assert.equal( - readFileSync(placeholderPath, "utf8"), - "", - `${directory}/${entry} must remain empty`, - ); - } - } -} -for (const directory of ["graders", "research"]) { - assert.deepEqual(trackedEntries(directory), ["README.md"], directory); +for (const path of trackedFiles.filter((path) => path.endsWith("/.gitkeep"))) { + assert.equal(readFileSync(resolve(root, path), "utf8"), "", `${path} must remain empty`); } const forbidden = [ diff --git a/.github/verify.test.mjs b/.github/verify.test.mjs index 527e5cd..b52b77b 100644 --- a/.github/verify.test.mjs +++ b/.github/verify.test.mjs @@ -30,6 +30,17 @@ for (const path of [".github/product-behavior.js", ".githooks/eval-results.json" }); } +for (const path of ["LICENSE", "AGENTS.md", "SECURITY.md"]) { + test(`rejects a directory replacing the file: ${path}`, () => { + const result = fixture((root) => { + rmSync(join(root, path)); + mkdirSync(join(root, path)); + writeFileSync(join(root, path, "eval-results.json"), "{}"); + }); + assert.notEqual(result.status, 0, result.stdout); + }); +} + test("accepts the current repository contract", () => { const result = fixture(); assert.equal(result.status, 0, result.stderr);