tractatus/scripts/hook-validators/validate-credentials.js
TheFlow a4db3e62ec
Some checks are pending
CI / Run Tests (push) Waiting to run
CI / Lint Code (push) Waiting to run
CI / CSP Compliance Check (push) Waiting to run
chore(vendor-policy): sweep project-self GitHub URLs to Codeberg (partial)
Addresses the documentation-layer gap after Phase A/B moved the git REMOTE from
GitHub to Codeberg but left ~100 project-self GitHub URLs embedded in markdown,
HTML, JS, and Python files. The remote-layer migration was generalised as
"GitHub is gone from the codebase" without verifying the content layer.

22 files swept in this commit. 27 additional files hold pre-existing inst_016/017/018
or inst_084 debt that would transfer on touch (hook whole-file scan). Those
await a companion hygiene-first commit before their GitHub->Codeberg flip
can land cleanly.

Sweep scope this commit:
  - README.md, SECURITY.md
  - 3 For-Claude-Web bundle files (GitHub URLs noted as "separate concern" in
    today's earlier licence-swap commits)
  - docs/markdown/deployment-guide.md
  - docs/AUTOMATED_SYNC_SETUP, PLURALISM_CHECKLIST, github/AGENT_LIGHTNING_README
  - docs/business-intelligence/governance-bi-tools
  - docs/outreach/EXECUTIVE-BRIEF-BI-GOVERNANCE (+ v2)
  - docs/research/ARCHITECTURAL-SAFEGUARDS-*
  - email-templates/README.md, base-template.html
  - 3 scripts/seed-*-blog-post.js (blog-seeding scripts)
  - scripts/upload-document.js
  - SESSION_HANDOFF_2025-10-23_FRAMEWORK_ANALYSIS.md
  - SECURITY_INCIDENT_POST_MORTEM_2025-10-21.md

Pattern swaps (longest-first):
  github.com/AgenticGovernance/tractatus-framework/issues -> codeberg.org/mysovereignty/tractatus-framework/issues
  github.com/AgenticGovernance/tractatus-framework/discussions -> .../issues (Codeberg has no discussions feature)
  github.com/AgenticGovernance/tractatus-framework.git -> codeberg.org/mysovereignty/tractatus-framework.git
  github.com/AgenticGovernance/tractatus-framework -> codeberg.org/mysovereignty/tractatus-framework
  git@github.com:AgenticGovernance/... -> git@codeberg.org:mysovereignty/...
  github.com/AgenticGovernance/tractatus (old org/repo path) -> codeberg.org/mysovereignty/tractatus-framework
  AgenticGovernance/tractatus-framework (bare) -> mysovereignty/tractatus-framework

Hook validator update (scripts/hook-validators/validate-credentials.js):
  PROTECTED_VALUES.github_org:  'AgenticGovernance'  -> 'mysovereignty'
  PROTECTED_VALUES.license:     'Apache License 2.0' -> EUPL-1.2 long form
  URL detection regex:          /github\.com\/.../   -> /codeberg\.org\/.../
  Placeholder checks + error messages updated to reflect Codeberg as
  authoritative post-migration host. Key names (e.g. `github_org`) retained
  for backward compatibility with validate-file-edit.js.

Held back from this commit (27 files total, documented reasons):

  11 historical session handoffs / closedown docs / incident reports
    (2025-10 through 2026-02) — modifying them rewrites the record to contain
    URLs that did not exist at the time of writing, AND ownership of their
    pre-existing inst_084 exposures transfers on touch.

  8 live-content docs with pre-existing inst_084 debt (port/API-endpoint/
    file-path exposures): docs/markdown/case-studies.md, technical-architecture,
    introduction-to-the-tractatus-framework, implementation-guide-v1.1,
    docs/plans/integrated-implementation-roadmap-2025, docs/governance/*,
    docs/ANTHROPIC_*, docs/GOVERNANCE_SERVICE_*, docs/RESEARCH_DOCUMENTATION_*,
    deployment-quickstart/*.

  8 live-content docs with pre-existing inst_016/017/018 debt:
    CHANGELOG.md, CONTRIBUTING.md, docs/LAUNCH_ANNOUNCEMENT, LAUNCH_CHECKLIST,
    PHASE_4_REPOSITORY_ANALYSIS, PHASE_6_SUMMARY, docs/plans/research-enhancement-
    roadmap-2025, docs/case-studies/pre-publication-audit-oct-2025.

  Also NOT in this commit (separate concerns):
  - scripts/add-inst-084-github-url-protection.js (detection-rule logic needs
    framework-level decision on post-migration semantics).
  - .claude/* (framework state).
  - docs/PRODUCTION_DOCUMENTS_EXPORT.json (DB dump).
  - package-lock.json (npm sponsor URLs, third-party).
  - .git/config embedded credentials (requires out-of-band rotation on both
    remote hosts + auth-strategy decision; user-action task).

Context: today's EUPL-1.2 sweep closed the licence-text-content layer
(5c386d0d / 6d49bfbf / ab0a6af4 / 4c1a26e8). This commit starts closing the
matching vendor-URL-content layer. Next: hygiene-first pass on the 16
live-content docs held back, then a second URL-flip pass on them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:53:13 +12:00

221 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Credential & Attribution Validator
*
* Prevents unauthorized changes to:
* - Copyright attribution
* - GitHub repository URLs
* - Contact emails
* - Domain names
* - API keys/credentials
* - Legal entity names
*
* Architectural enforcement to prevent Claude from arbitrarily changing
* important credentials without human confirmation.
*
* Created: 2025-10-18
* Incident: Footer component overwrote copyright from "John G Stroh" to "Tractatus AI Safety Framework"
*/
const fs = require('fs');
const path = require('path');
// IMMUTABLE CREDENTIALS - These values MUST NOT be changed without human approval
const PROTECTED_VALUES = {
copyright_holder: 'John G Stroh',
// Key names retained for backward compatibility; values updated to reflect
// the post-migration authoritative host (Codeberg) and licence (EUPL-1.2).
github_org: 'mysovereignty',
github_repo: 'tractatus-framework',
primary_email: 'hello@agenticgovernance.digital',
support_email: 'support@agenticgovernance.digital',
domain: 'agenticgovernance.digital',
license: 'European Union Public Licence, Version 1.2 (EUPL-1.2)',
};
// File patterns that commonly contain credentials
const CREDENTIAL_FILES = [
'public/js/components/footer.js',
'LICENSE',
'package.json',
'public/**/*.html',
'docs/**/*.md',
];
/**
* Validate that protected values haven't been arbitrarily changed
*/
function validateCredentials(filePath, fileContent) {
const violations = [];
// Check for incorrect copyright attribution
const copyrightRegex = /©.*?(\d{4})\s+([^.\n<]+)/g;
let match;
while ((match = copyrightRegex.exec(fileContent)) !== null) {
const holder = match[2].trim();
// Allow "John G Stroh" or "John G. Stroh" (with period)
if (!holder.includes('John G') && !holder.includes(PROTECTED_VALUES.copyright_holder)) {
violations.push({
type: 'COPYRIGHT_MISMATCH',
line: getLineNumber(fileContent, match.index),
found: holder,
expected: PROTECTED_VALUES.copyright_holder,
message: `Copyright holder must be "${PROTECTED_VALUES.copyright_holder}" (from LICENSE file)`
});
}
}
// Check for incorrect Codeberg URLs (authoritative host post-migration)
const repoRegex = /codeberg\.org\/([^\/\s"']+)\/([^\/\s"'<]+)/g;
while ((match = repoRegex.exec(fileContent)) !== null) {
const org = match[1];
const repo = match[2];
// Check if it's supposed to be our repo but has wrong values
if ((org !== PROTECTED_VALUES.github_org || repo !== PROTECTED_VALUES.github_repo) &&
(org.includes('tractatus') || repo.includes('tractatus'))) {
violations.push({
type: 'REPO_URL_INCORRECT',
line: getLineNumber(fileContent, match.index),
found: `${org}/${repo}`,
expected: `${PROTECTED_VALUES.github_org}/${PROTECTED_VALUES.github_repo}`,
message: `Repository reference must be "${PROTECTED_VALUES.github_org}/${PROTECTED_VALUES.github_repo}" on codeberg.org`
});
}
}
// Check for placeholder repo URLs
if (fileContent.includes('codeberg.org/yourusername') ||
fileContent.includes('codeberg.org/your-org')) {
violations.push({
type: 'REPO_PLACEHOLDER',
message: 'Repository URL contains placeholder - must use actual repository URL',
expected: `https://codeberg.org/${PROTECTED_VALUES.github_org}/${PROTECTED_VALUES.github_repo}`
});
}
// Check for incorrect domain references
const domainRegex = /https?:\/\/([a-z0-9-]+\.)+[a-z]{2,}/gi;
while ((match = domainRegex.exec(fileContent)) !== null) {
const url = match[0];
// Skip CDNs, external services, etc.
if (url.includes('tractatus') && !url.includes(PROTECTED_VALUES.domain)) {
violations.push({
type: 'DOMAIN_INCORRECT',
line: getLineNumber(fileContent, match.index),
found: url,
expected: `https://${PROTECTED_VALUES.domain}`,
message: `Domain must be "${PROTECTED_VALUES.domain}"`
});
}
}
return violations;
}
/**
* Get line number from character index
*/
function getLineNumber(content, index) {
return content.substring(0, index).split('\n').length;
}
/**
* Main validation function
*/
function validate(filePath, operation) {
// Only validate files that might contain credentials
const shouldValidate = CREDENTIAL_FILES.some(pattern => {
if (pattern.includes('*')) {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return regex.test(filePath);
}
return filePath.includes(pattern);
});
if (!shouldValidate) {
return { valid: true };
}
// Read file content
let content;
try {
content = fs.readFileSync(filePath, 'utf-8');
} catch (error) {
// File doesn't exist yet (being created)
return { valid: true };
}
// Validate credentials
const violations = validateCredentials(filePath, content);
if (violations.length > 0) {
return {
valid: false,
violations,
message: formatViolations(filePath, violations)
};
}
return { valid: true };
}
/**
* Format violations for human-readable output
*/
function formatViolations(filePath, violations) {
let message = `\n❌ CREDENTIAL VIOLATION in ${filePath}\n\n`;
message += `PROTECTED CREDENTIALS ENFORCEMENT:\n`;
message += `The following values are IMMUTABLE and require human approval to change:\n\n`;
for (const violation of violations) {
message += ` ⚠️ ${violation.type}:\n`;
if (violation.line) {
message += ` Line: ${violation.line}\n`;
}
if (violation.found) {
message += ` Found: "${violation.found}"\n`;
}
message += ` Expected: "${violation.expected}"\n`;
message += ` Reason: ${violation.message}\n\n`;
}
message += `\n`;
message += `RESOLUTION:\n`;
message += `1. This change requires EXPLICIT human approval\n`;
message += `2. Verify the change is intentional and authorized\n`;
message += `3. If correcting an error, update to the protected values above\n`;
message += `4. If legitimately changing credentials, update PROTECTED_VALUES in:\n`;
message += ` scripts/hook-validators/validate-credentials.js\n`;
message += `\n`;
message += `Protected values are defined in LICENSE and package.json.\n`;
message += `Copyright holder: John G Stroh (LICENSE:189)\n`;
message += `Repository: mysovereignty/tractatus-framework (codeberg.org — authoritative host post-migration; US-vendor hosts forbidden per vendor policy)\n`;
return message;
}
// Export for use as module
module.exports = { validate, validateCredentials, PROTECTED_VALUES };
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: validate-credentials.js <file-path> <operation>');
process.exit(1);
}
const [filePath, operation] = args;
const result = validate(filePath, operation);
if (!result.valid) {
console.error(result.message);
process.exit(1);
}
process.exit(0);
}