OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / verify-blind-legacy-retirement-policy.mjs

376 lines · javascript ref:
#!/usr/bin/env node

import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { ABI_RELEASE_BLOCKERS } from '../packages/blind-protocol/registry.js'
import { PRIVATE_IPC_STATUS } from '../packages/blind-ipc/index.js'
import { BLIND_CLIENT_BROWSER_ARTIFACT_STATUS } from '../packages/blind-client/browser-artifact.js'
import { BLIND_CELL_RUNTIME_BLOCKERS } from '../packages/blind-daemon/cell-runtime-adapter.js'
import { BLIND_CORE_RUNTIME_BLOCKERS } from '../packages/blind-daemon/core-runtime-adapter.js'
import { BLIND_CORE_STORAGE_BLOCKERS } from '../packages/blind-daemon/core-storage-engine.js'
import { BLIND_INBOX_RUNTIME_BLOCKERS } from '../packages/blind-daemon/inbox-runtime-adapter.js'
import { BLIND_INBOX_STORAGE_BLOCKERS } from '../packages/blind-daemon/inbox-storage-engine.js'
import { PRODUCTION_RUNTIME_EXCLUSIONS } from '../packages/blind-daemon/production-runtime.js'
import { BLIND_CELL_STORAGE_PRODUCTION_BLOCKERS } from '../packages/blind-daemon/storage-engine.js'

const here = path.dirname(fileURLToPath(import.meta.url))

export const LEGACY_RETIREMENT_POLICY_PATH = path.resolve(
  here,
  '../deploy/blind/legacy-retirement-policy.json'
)

const GATE_IDS = Object.freeze(['G0', 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', 'G8'])
const COMPONENT_IDS = Object.freeze([
  'legacy-service-plane',
  'outboxlog',
  'witness-repair',
  'shard-custody',
  'seed-storage-gateway',
  'catalog-federation-index',
  'notify',
  'poker',
  'compute-trust-services',
  'legacy-circuit-forward',
  'edge-transports',
  'legacy-client-verifier',
  'operator-control-economics',
  'peerit-legacy-consumer-plane'
])
const FORBIDDEN_CLAIM_IDS = Object.freeze([
  'self-description-proves-runtime',
  'signed-roster-proves-independence',
  'ciphertext-label-proves-blindness',
  'multiple-keys-prove-operators',
  'attestation-proves-retrieval',
  'tor-advertisement-proves-anonymity',
  'client-verification-proves-availability',
  'unseed-proves-erasure',
  'opaque-poker-proves-generic',
  'hand-edited-authorization'
])
const REAL_EVIDENCE_IDS = Object.freeze([
  'executable-production-composition',
  'plaintext-sentinel-absence',
  'canonical-capability-result',
  'transactional-wal-recovery',
  'private-ipc-peer-credentials',
  'retrieval-proof',
  'independent-operator-failure-domain',
  'cross-runtime-client-parity',
  'migration-restore-drill',
  'observed-zero-traffic'
])
const RETIREMENT_SEQUENCE_IDS = Object.freeze(['freeze', 'compose', 'migrate', 'drain', 'remove'])
const DISPOSITIONS = new Set([
  'migrate-then-retire',
  'externalize-from-base',
  'retain-isolated',
  'adapt-and-retain'
])

const BLOCKER_AUTHORITIES = Object.freeze([
  Object.freeze({ source: 'blind-protocol.ABI_RELEASE_BLOCKERS', blockers: ABI_RELEASE_BLOCKERS }),
  Object.freeze({ source: 'blind-ipc.PRIVATE_IPC_STATUS.releaseBlockers', blockers: PRIVATE_IPC_STATUS.releaseBlockers }),
  Object.freeze({ source: 'blind-client.BLIND_CLIENT_BROWSER_ARTIFACT_STATUS.releaseBlockers', blockers: BLIND_CLIENT_BROWSER_ARTIFACT_STATUS.releaseBlockers }),
  Object.freeze({ source: 'blind-daemon.PRODUCTION_RUNTIME_EXCLUSIONS', blockers: PRODUCTION_RUNTIME_EXCLUSIONS }),
  Object.freeze({ source: 'blind-daemon.BLIND_CELL_RUNTIME_BLOCKERS', blockers: BLIND_CELL_RUNTIME_BLOCKERS }),
  Object.freeze({ source: 'blind-daemon.BLIND_CELL_STORAGE_PRODUCTION_BLOCKERS', blockers: BLIND_CELL_STORAGE_PRODUCTION_BLOCKERS }),
  Object.freeze({ source: 'blind-daemon.BLIND_INBOX_RUNTIME_BLOCKERS', blockers: BLIND_INBOX_RUNTIME_BLOCKERS }),
  Object.freeze({ source: 'blind-daemon.BLIND_INBOX_STORAGE_BLOCKERS', blockers: BLIND_INBOX_STORAGE_BLOCKERS }),
  Object.freeze({ source: 'blind-daemon.BLIND_CORE_RUNTIME_BLOCKERS', blockers: BLIND_CORE_RUNTIME_BLOCKERS }),
  Object.freeze({ source: 'blind-daemon.BLIND_CORE_STORAGE_BLOCKERS', blockers: BLIND_CORE_STORAGE_BLOCKERS })
])

// These are code-owned blockers, not deployment JSON. Each must be replaced by
// an executable evidence authority before legacy deletion can ever authorize.
export const LEGACY_RETIREMENT_EVIDENCE_BLOCKERS = Object.freeze([
  'G2_BLINDNESS_SENTINEL_EVIDENCE_AUTHORITY_UNASSEMBLED',
  'G3_LEGACY_SEMANTIC_PARITY_EVIDENCE_AUTHORITY_UNASSEMBLED',
  'G4_LEGACY_DURABILITY_SCALE_EVIDENCE_AUTHORITY_UNASSEMBLED',
  'G5_INDEPENDENT_OPERATOR_EVIDENCE_AUTHORITY_UNASSEMBLED',
  'G6_CONSUMER_PARITY_EVIDENCE_AUTHORITY_UNASSEMBLED',
  'G7_MIGRATION_RESTORE_EVIDENCE_AUTHORITY_UNASSEMBLED',
  'G8_ZERO_TRAFFIC_RETIREMENT_EVIDENCE_AUTHORITY_UNASSEMBLED'
])

export async function loadLegacyRetirementPolicy (policyPath = LEGACY_RETIREMENT_POLICY_PATH) {
  const bytes = await fs.readFile(policyPath)
  if (bytes.byteLength === 0 || bytes.byteLength > 1024 * 1024) {
    policyFailure('legacy retirement policy must be 1..1048576 bytes')
  }
  let policy
  try {
    policy = JSON.parse(bytes.toString('utf8'))
  } catch (error) {
    policyFailure(`legacy retirement policy is not valid JSON: ${error.message}`)
  }
  return validateLegacyRetirementPolicy(policy)
}

export function validateLegacyRetirementPolicy (policy) {
  assertPlainObject(policy, 'policy')
  rejectAuthorizationBooleans(policy)
  assertExactKeys(policy, [
    'policyVersion',
    'policyId',
    'decisionAuthority',
    'authorizationRule',
    'gates',
    'components',
    'forbiddenClaims',
    'realEvidence',
    'retirementSequence'
  ], 'policy')
  if (policy.policyVersion !== 1) policyFailure('policyVersion must equal 1')
  if (policy.policyId !== 'hiverelay.blind.legacy-retirement.v1') {
    policyFailure('policyId does not match the registered retirement policy')
  }
  if (policy.decisionAuthority !== 'computed-production-blockers-only') {
    policyFailure('decisionAuthority must remain computed-production-blockers-only')
  }

  assertPlainObject(policy.authorizationRule, 'authorizationRule')
  assertExactKeys(policy.authorizationRule, [
    'computedBy',
    'rule',
    'editableDecisionFields',
    'blockerSources',
    'retirementEvidenceSource'
  ], 'authorizationRule')
  if (policy.authorizationRule.computedBy !== 'scripts/verify-blind-legacy-retirement-policy.mjs') {
    policyFailure('authorizationRule.computedBy does not name the registered verifier')
  }
  if (policy.authorizationRule.editableDecisionFields !== 'forbidden') {
    policyFailure('authorizationRule.editableDecisionFields must equal forbidden')
  }
  if (policy.authorizationRule.retirementEvidenceSource !==
      'blind-legacy-retirement.LEGACY_RETIREMENT_EVIDENCE_BLOCKERS') {
    policyFailure('authorizationRule.retirementEvidenceSource does not name the code-owned authority')
  }
  assertNonEmptyString(policy.authorizationRule.rule, 'authorizationRule.rule')
  assertExactStringArray(
    policy.authorizationRule.blockerSources,
    BLOCKER_AUTHORITIES.map(authority => authority.source),
    'authorizationRule.blockerSources'
  )

  assertExactIdRows(policy.gates, GATE_IDS, 'gates', row => {
    assertExactKeys(row, ['id', 'title', 'requirement', 'evidence'], `gate ${row.id}`)
    assertNonEmptyString(row.title, 'gate.title')
    assertNonEmptyString(row.requirement, 'gate.requirement')
    assertNonEmptyStringArray(row.evidence, 'gate.evidence')
  })
  const gateIds = new Set(GATE_IDS)
  assertExactIdRows(policy.components, COMPONENT_IDS, 'components', row => {
    assertExactKeys(row, [
      'id',
      'legacyPaths',
      'disposition',
      'replacement',
      'extract',
      'deletionGates'
    ], `component ${row.id}`)
    assertNonEmptyStringArray(row.legacyPaths, 'component.legacyPaths')
    if (!DISPOSITIONS.has(row.disposition)) {
      policyFailure(`component ${row.id} has an unknown disposition`)
    }
    assertNonEmptyStringArray(row.replacement, 'component.replacement')
    assertNonEmptyStringArray(row.extract, 'component.extract')
    assertNonEmptyStringArray(row.deletionGates, 'component.deletionGates')
    if (!row.deletionGates.includes('G8')) {
      policyFailure(`component ${row.id} must require G8 before removal`)
    }
    for (const gate of row.deletionGates) {
      if (!gateIds.has(gate)) policyFailure(`component ${row.id} references unknown gate ${gate}`)
    }
  })
  assertExactIdRows(policy.forbiddenClaims, FORBIDDEN_CLAIM_IDS, 'forbiddenClaims', row => {
    assertExactKeys(row, [
      'id',
      'claim',
      'whyInvalid',
      'requiredEvidence'
    ], `forbidden claim ${row.id}`)
    assertNonEmptyString(row.claim, 'forbiddenClaim.claim')
    assertNonEmptyString(row.whyInvalid, 'forbiddenClaim.whyInvalid')
    assertNonEmptyStringArray(row.requiredEvidence, 'forbiddenClaim.requiredEvidence')
  })
  assertExactIdRows(policy.realEvidence, REAL_EVIDENCE_IDS, 'realEvidence', row => {
    assertExactKeys(row, ['id', 'evidence', 'proves', 'codeAnchors'], `real evidence ${row.id}`)
    assertNonEmptyString(row.evidence, 'realEvidence.evidence')
    assertNonEmptyStringArray(row.proves, 'realEvidence.proves')
    assertNonEmptyStringArray(row.codeAnchors, 'realEvidence.codeAnchors')
  })
  assertExactIdRows(policy.retirementSequence, RETIREMENT_SEQUENCE_IDS, 'retirementSequence', row => {
    assertExactKeys(row, ['id', 'requires', 'action'], `retirement sequence ${row.id}`)
    assertNonEmptyStringArray(row.requires, 'retirementSequence.requires')
    assertNonEmptyString(row.action, 'retirementSequence.action')
    for (const gate of row.requires) {
      if (!gateIds.has(gate)) policyFailure(`retirement phase ${row.id} references unknown gate ${gate}`)
    }
  })
  assertExactStringArray(
    policy.retirementSequence.at(-1).requires,
    GATE_IDS,
    'retirementSequence.remove.requires'
  )
  return policy
}

export function productionRetirementBlockers () {
  const rows = []
  for (const authority of BLOCKER_AUTHORITIES) {
    if (!Array.isArray(authority.blockers)) {
      policyFailure(`production blocker source ${authority.source} is not an array`)
    }
    for (const code of authority.blockers) {
      assertNonEmptyString(code, `production blocker from ${authority.source}`)
      rows.push(Object.freeze({ source: authority.source, code }))
    }
  }
  return Object.freeze(rows)
}

export function evaluateLegacyRetirement (policy) {
  validateLegacyRetirementPolicy(policy)
  const productionBlockers = productionRetirementBlockers()
  const evidenceBlockers = LEGACY_RETIREMENT_EVIDENCE_BLOCKERS.map(code => Object.freeze({
    source: 'blind-legacy-retirement.LEGACY_RETIREMENT_EVIDENCE_BLOCKERS',
    code
  }))
  const blockers = Object.freeze([...productionBlockers, ...evidenceBlockers])
  const blockingCodes = Object.freeze([...new Set(blockers.map(row => row.code))].sort())
  const blockerSources = Object.freeze([...new Set(blockers.map(row => row.source))].sort())
  const authorized = blockers.length === 0
  return Object.freeze({
    policyId: policy.policyId,
    policyVersion: policy.policyVersion,
    decisionAuthority: policy.decisionAuthority,
    status: authorized ? 'authorized' : 'blocked',
    authorized,
    blockerCount: blockers.length,
    productionBlockerCount: productionBlockers.length,
    retirementEvidenceBlockerCount: evidenceBlockers.length,
    blockingCodes,
    blockerSources,
    requiredGates: GATE_IDS,
    componentCount: COMPONENT_IDS.length,
    blockers
  })
}

export function assertLegacyRetirementAuthorized (policy) {
  const report = evaluateLegacyRetirement(policy)
  if (report.authorized) return report
  const error = new Error(`legacy retirement is blocked by ${report.blockerCount} code-owned blockers`)
  error.code = 'BLIND_LEGACY_RETIREMENT_BLOCKED'
  error.report = report
  throw error
}

function rejectAuthorizationBooleans (value, at = 'policy') {
  if (Array.isArray(value)) {
    value.forEach((entry, index) => rejectAuthorizationBooleans(entry, `${at}[${index}]`))
    return
  }
  if (!value || typeof value !== 'object') return
  for (const [key, entry] of Object.entries(value)) {
    const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase()
    if (typeof entry === 'boolean' && AUTHORIZATION_BOOLEAN_KEYS.has(normalized)) {
      policyFailure(`${at}.${key} is a forbidden hand-editable authorization boolean`)
    }
    rejectAuthorizationBooleans(entry, `${at}.${key}`)
  }
}

const AUTHORIZATION_BOOLEAN_KEYS = new Set([
  'authorized',
  'authorization',
  'allowdeletion',
  'candelete',
  'deletionauthorized',
  'retirementauthorized',
  'legacyretirementauthorized',
  'approvedfordeletion',
  'retirementready',
  'deletionready'
])

function assertExactIdRows (rows, expectedIds, field, inspect) {
  if (!Array.isArray(rows)) policyFailure(`${field} must be an array`)
  assertExactStringArray(rows.map(row => {
    assertPlainObject(row, `${field} row`)
    assertNonEmptyString(row.id, `${field} row id`)
    return row.id
  }), expectedIds, `${field} ids`)
  for (const row of rows) inspect(row)
}

function assertExactStringArray (actual, expected, field) {
  if (!Array.isArray(actual) || actual.length !== expected.length ||
      actual.some((value, index) => value !== expected[index])) {
    policyFailure(`${field} must equal the registered ordered values`)
  }
}

function assertExactKeys (value, expected, field) {
  assertPlainObject(value, field)
  const actual = Object.keys(value).sort()
  const sortedExpected = [...expected].sort()
  if (actual.length !== sortedExpected.length ||
      actual.some((key, index) => key !== sortedExpected[index])) {
    policyFailure(`${field} must contain exactly the registered fields`)
  }
}

function assertNonEmptyStringArray (value, field) {
  if (!Array.isArray(value) || value.length === 0) policyFailure(`${field} must be a non-empty array`)
  const seen = new Set()
  for (const entry of value) {
    assertNonEmptyString(entry, field)
    if (seen.has(entry)) policyFailure(`${field} contains a duplicate value`)
    seen.add(entry)
  }
}

function assertNonEmptyString (value, field) {
  if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) {
    policyFailure(`${field} must be a non-empty trimmed string`)
  }
}

function assertPlainObject (value, field) {
  if (!value || typeof value !== 'object' || Array.isArray(value) ||
      (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
    policyFailure(`${field} must be a plain object`)
  }
}

function policyFailure (message) {
  const error = new Error(message)
  error.code = 'BLIND_LEGACY_RETIREMENT_POLICY_INVALID'
  throw error
}

async function main () {
  const args = new Set(process.argv.slice(2))
  for (const arg of args) {
    if (arg !== '--json' && arg !== '--require-authorized') {
      throw new Error(`unknown argument: ${arg}`)
    }
  }
  const policy = await loadLegacyRetirementPolicy()
  const report = evaluateLegacyRetirement(policy)
  process.stdout.write(`${JSON.stringify(report, null, args.has('--json') ? 2 : 0)}\n`)
  if (args.has('--require-authorized') && !report.authorized) process.exitCode = 1
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  main().catch(error => {
    process.stderr.write(`${error.stack || error.message}\n`)
    process.exitCode = 1
  })
}