OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / fleet / plan-reprovision.mjs

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

import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

export const REPROVISION_INPUT_SCHEMA = 'hiverelay/fleet-reprovision-input/v1'
export const REPROVISION_PLAN_SCHEMA = 'hiverelay/fleet-reprovision-plan/v1'

const DIGEST = /^[a-f0-9]{64}$/
const COMMIT = /^[a-f0-9]{40}$/
const DURABLE_FAMILIES = Object.freeze(['CELL', 'INBOX', 'CORE'])
const REQUIRED_RELAY_FIELDS = Object.freeze([
  'operatorId',
  'hostId',
  'failureDomain',
  'roleProfile',
  'storageGeneration',
  'admissionClass',
  'retentionDisposition',
  'rootId'
])

export function buildFleetReprovisionPlan (input = {}) {
  const blockers = []
  const addBlocker = (id, detail) => {
    if (!blockers.some(blocker => blocker.id === id)) blockers.push({ id, detail })
  }
  const inventory = isObject(input.inventory) ? input.inventory : {}
  const relays = Array.isArray(inventory.relays) ? inventory.relays : []
  const targetRelay = nonEmpty(input.targetRelay) ? input.targetRelay : null
  const targetMatches = relays.filter(relay => relay?.name === targetRelay)
  const target = targetMatches.length === 1 ? targetMatches[0] : null
  const artifact = isObject(input.artifact) ? input.artifact : {}
  const targetContracts = isObject(input.targetContracts) ? input.targetContracts : {}

  if (input.schema !== REPROVISION_INPUT_SCHEMA) {
    addBlocker('INPUT_SCHEMA_INVALID', `schema must be ${REPROVISION_INPUT_SCHEMA}`)
  }
  if (!targetRelay) addBlocker('TARGET_RELAY_REQUIRED', 'name exactly one relay')
  else if (targetMatches.length === 0) {
    addBlocker('TARGET_RELAY_NOT_FOUND', `${targetRelay} is not present in the inventory`)
  } else if (targetMatches.length > 1) {
    addBlocker('TARGET_RELAY_AMBIGUOUS', `${targetRelay} appears ${targetMatches.length} times`)
  }

  if (!COMMIT.test(artifact.sourceCommit || '')) {
    addBlocker('SOURCE_COMMIT_REQUIRED', 'bind the plan to one exact 40-character source commit')
  }
  if (!DIGEST.test(artifact.sha256 || '')) {
    addBlocker('RC_ARTIFACT_DIGEST_REQUIRED', 'bind the plan to one immutable SHA-256 release artifact')
  }
  if (!nonEmpty(artifact.releaseId)) {
    addBlocker('RC_RELEASE_ID_REQUIRED', 'bind the plan to one immutable release identity')
  }
  if (!positiveInteger(artifact.releaseSequence)) {
    addBlocker('RC_RELEASE_SEQUENCE_REQUIRED', 'bind the plan to a positive monotonic release sequence')
  }
  if (!validTargetContracts(targetContracts)) {
    addBlocker(
      'TARGET_CONTRACT_HASHES_REQUIRED',
      'specHash, storeFormatHash, and privateIpcV2Hash must be exact SHA-256 values'
    )
  }

  if (!DIGEST.test(inventory.evidenceDigest || '') || inventory.signatureVerified !== true) {
    addBlocker(
      'SIGNED_FLEET_INVENTORY_REQUIRED',
      'inventory needs an externally verified evidence digest and operator signature'
    )
  }
  if (!validTimestamp(inventory.observedAt)) {
    addBlocker('FLEET_INVENTORY_TIMESTAMP_REQUIRED', 'inventory observedAt must be a valid timestamp')
  }
  inspectInventoryFreshness(inventory.observedAt, input, addBlocker)
  if (relays.length === 0) {
    addBlocker('FLEET_INVENTORY_EMPTY', 'inventory must contain at least one relay')
  }
  const duplicateRelayNames = duplicateValues(relays.map(relay => relay?.name).filter(nonEmpty))
  if (duplicateRelayNames.length > 0) {
    addBlocker(
      'DUPLICATE_RELAY_NAMES',
      `relay names must be unique: ${duplicateRelayNames.join(', ')}`
    )
  }

  const incompleteRelays = relays
    .map(relay => inspectRelay(relay))
    .filter(result => result.missing.length > 0)
  if (incompleteRelays.length > 0) {
    addBlocker(
      'RELAY_METADATA_INCOMPLETE',
      incompleteRelays.map(result => `${result.name}: ${result.missing.join(', ')}`).join('; ')
    )
  }
  if (target && inspectRelay(target).missing.includes('backup')) {
    addBlocker(
      'TARGET_BACKUP_PROOF_REQUIRED',
      `${target.name} needs off-node backup and isolated-restore evidence`
    )
  }

  inspectRetention(input.retentionCensus, addBlocker)
  inspectCapacity(input.capacityProjection, addBlocker)
  inspectReplicaFloors(input.replicaProjection, addBlocker)
  inspectRestore(input.restore, addBlocker)
  inspectRollback(input.rollback, addBlocker)

  if (!nonEmpty(input.newRootId)) {
    addBlocker('NEW_ROOT_ID_REQUIRED', 'name the disposable empty root before an operator acts')
  } else {
    const rootOwner = relays.find(relay => relay?.rootId === input.newRootId)
    if (rootOwner) {
      addBlocker(
        'NEW_ROOT_COLLIDES_WITH_EXISTING_ROOT',
        `${input.newRootId} is already owned by ${rootOwner.name || '<unnamed>'}`
      )
    }
  }

  const inventorySummary = summarizeInventory(relays)
  const body = {
    schema: REPROVISION_PLAN_SCHEMA,
    inputSchema: input.schema || null,
    dryRun: true,
    status: blockers.length === 0 ? 'ready-for-operator-review' : 'blocked',
    evidenceClass: 'local-static-plan-not-observed-reprovision',
    claimBoundary: 'This report performs no SSH, network, channel, key, service, root, or fleet mutation. A blocker-free plan is still not deployment authority, observed convergence, PG-5, PG-7, release readiness, or GA evidence.',
    targetRelay,
    newRootId: nonEmpty(input.newRootId) ? input.newRootId : null,
    artifact: {
      releaseId: nonEmpty(artifact.releaseId) ? artifact.releaseId : null,
      releaseSequence: positiveInteger(artifact.releaseSequence) ? artifact.releaseSequence : null,
      sourceCommit: COMMIT.test(artifact.sourceCommit || '') ? artifact.sourceCommit : null,
      sha256: DIGEST.test(artifact.sha256 || '') ? artifact.sha256 : null
    },
    targetContracts: validTargetContracts(targetContracts) ? targetContracts : null,
    inventory: {
      evidenceDigest: DIGEST.test(inventory.evidenceDigest || '') ? inventory.evidenceDigest : null,
      signatureVerified: inventory.signatureVerified === true,
      observedAt: validTimestamp(inventory.observedAt) ? inventory.observedAt : null,
      evaluatedAt: validTimestamp(input.evaluatedAt) ? input.evaluatedAt : null,
      maximumAgeSeconds: positiveInteger(input.maximumInventoryAgeSeconds)
        ? input.maximumInventoryAgeSeconds
        : null,
      ...inventorySummary
    },
    projections: summarizeProjections(input),
    blockers,
    proposedSequence: proposedSequence(targetRelay),
    authority: {
      authorizesMutation: false,
      authorizesReprovision: false,
      authorizesRelease: false,
      releaseReady: false,
      pg5Passed: false,
      pg7Passed: false,
      requiredNextAuthority: blockers.length === 0
        ? 'independent review, immutable RC acceptance, and an explicit human fleet-operation lease'
        : 'supply the missing local evidence; do not operate the fleet'
    }
  }
  return { ...body, planDigest: sha256(stableStringify(body)) }
}

export function verifyFleetReprovisionPlanDigest (report) {
  if (!isObject(report) || !DIGEST.test(report.planDigest || '')) return false
  const { planDigest, ...body } = report
  return planDigest === sha256(stableStringify(body))
}

function inspectRelay (relay) {
  const name = nonEmpty(relay?.name) ? relay.name : '<unnamed>'
  const missing = []
  for (const field of REQUIRED_RELAY_FIELDS) {
    if (!nonEmpty(relay?.[field])) missing.push(field)
  }
  if (!validTargetContracts(relay?.protocolHashes)) missing.push('protocolHashes')
  if (!isObject(relay?.clockTrust) ||
      relay.clockTrust.trusted !== true ||
      !DIGEST.test(relay.clockTrust.evidenceDigest || '')) {
    missing.push('clockTrust')
  }
  if (!isObject(relay?.capacity) ||
      !nonNegativeInteger(relay.capacity.observedUsableBytes) ||
      !nonNegativeInteger(relay.capacity.observedUsedBytes) ||
      relay.capacity.observedUsedBytes > relay.capacity.observedUsableBytes ||
      !DIGEST.test(relay.capacity.evidenceDigest || '')) {
    missing.push('capacity')
  }
  if (!isObject(relay?.backup) ||
      relay.backup.offNode !== true ||
      relay.backup.isolatedRestorePassed !== true ||
      !DIGEST.test(relay.backup.evidenceDigest || '')) {
    missing.push('backup')
  }
  return { name, missing }
}

function inspectRetention (retention, addBlocker) {
  if (!isObject(retention) ||
      !DIGEST.test(retention.evidenceDigest || '') ||
      !nonNegativeInteger(retention.uniqueUnknownObjects)) {
    addBlocker(
      'RETENTION_CENSUS_REQUIRED',
      'supply a digest-bound census with an exact uniqueUnknownObjects count'
    )
    return
  }
  if (retention.uniqueUnknownObjects !== 0) {
    addBlocker(
      'UNIQUE_OR_UNKNOWN_OBJECTS_PRESENT',
      `${retention.uniqueUnknownObjects} unique or unknown objects remain`
    )
  }
}

function inspectInventoryFreshness (observedAt, input, addBlocker) {
  if (!validTimestamp(input.evaluatedAt) || !positiveInteger(input.maximumInventoryAgeSeconds)) {
    addBlocker(
      'INVENTORY_FRESHNESS_WINDOW_REQUIRED',
      'supply evaluatedAt and a positive maximumInventoryAgeSeconds'
    )
    return
  }
  if (!validTimestamp(observedAt)) return
  const ageMillis = Date.parse(input.evaluatedAt) - Date.parse(observedAt)
  const maximumAgeMillis = input.maximumInventoryAgeSeconds * 1000
  const maximumFutureSkewMillis = 300 * 1000
  if (ageMillis > maximumAgeMillis) {
    addBlocker(
      'FLEET_INVENTORY_STALE',
      `inventory age ${Math.floor(ageMillis / 1000)}s exceeds ${input.maximumInventoryAgeSeconds}s`
    )
  } else if (ageMillis < -maximumFutureSkewMillis) {
    addBlocker(
      'FLEET_INVENTORY_FROM_FUTURE',
      `inventory observedAt is ${Math.ceil(-ageMillis / 1000)}s ahead of evaluatedAt`
    )
  }
}

function inspectCapacity (projection, addBlocker) {
  if (!isObject(projection) ||
      !DIGEST.test(projection.evidenceDigest || '') ||
      !nonNegativeInteger(projection.afterDrainFreeBytes) ||
      !nonNegativeInteger(projection.minimumFreeBytes)) {
    addBlocker(
      'CAPACITY_EVIDENCE_REQUIRED',
      'supply observed, digest-bound after-drain capacity and its minimum floor'
    )
    return
  }
  if (projection.afterDrainFreeBytes < projection.minimumFreeBytes) {
    addBlocker(
      'CAPACITY_FLOOR_NOT_MET',
      `${projection.afterDrainFreeBytes} after-drain bytes are below ${projection.minimumFreeBytes}`
    )
  }
}

function inspectReplicaFloors (projection, addBlocker) {
  if (!isObject(projection) ||
      !DIGEST.test(projection.evidenceDigest || '') ||
      !isObject(projection.afterDrainMinimumByFamily) ||
      !isObject(projection.floorByFamily)) {
    addBlocker(
      'REPLICA_FLOOR_EVIDENCE_REQUIRED',
      'supply digest-bound after-drain and required replica floors by durable family'
    )
    return
  }
  const missing = DURABLE_FAMILIES.filter(family =>
    !positiveInteger(projection.afterDrainMinimumByFamily[family]) ||
    !positiveInteger(projection.floorByFamily[family]))
  if (missing.length > 0) {
    addBlocker(
      'REPLICA_FLOOR_EVIDENCE_REQUIRED',
      `missing positive after-drain/floor values for ${missing.join(', ')}`
    )
    return
  }
  const violations = DURABLE_FAMILIES.filter(family =>
    projection.afterDrainMinimumByFamily[family] < projection.floorByFamily[family])
  if (violations.length > 0) {
    addBlocker(
      'REPLICA_FLOOR_NOT_MET',
      violations.map(family =>
        `${family} ${projection.afterDrainMinimumByFamily[family]} < ${projection.floorByFamily[family]}`
      ).join(', ')
    )
  }
}

function inspectRestore (restore, addBlocker) {
  if (!isObject(restore) ||
      !DIGEST.test(restore.evidenceDigest || '') ||
      restore.isolated !== true ||
      restore.passed !== true) {
    addBlocker(
      'ISOLATED_RESTORE_PROOF_REQUIRED',
      'supply digest-bound evidence for a passing isolated restore'
    )
  }
}

function inspectRollback (rollback, addBlocker) {
  if (!isObject(rollback) ||
      rollback.readerMode !== 'blind-plus-legacy-dual-read' ||
      !DIGEST.test(rollback.artifactSha256 || '')) {
    addBlocker(
      'D7_DUAL_READ_ROLLBACK_ARTIFACT_REQUIRED',
      'D-7 permits only an exact blind-plus-legacy dual-read rollback artifact'
    )
  }
  if (!isObject(rollback) ||
      !DIGEST.test(rollback.evidenceDigest || '') ||
      rollback.drillPassed !== true) {
    addBlocker(
      'ROLLBACK_DRILL_PROOF_REQUIRED',
      'supply digest-bound evidence for a passing rollback and forward-recovery drill'
    )
  }
  if (!isObject(rollback) || rollback.oldRootRetained !== true) {
    addBlocker(
      'OLD_ROOT_RETENTION_REQUIRED',
      'retain the old root through the dual-read, restore, and rollback window'
    )
  }
}

function summarizeInventory (relays) {
  const channels = {}
  const regions = new Set()
  let declaredNominalDiskGB = 0
  for (const relay of relays) {
    const channel = nonEmpty(relay?.channel) ? relay.channel : 'unknown'
    channels[channel] = (channels[channel] || 0) + 1
    if (nonEmpty(relay?.region)) regions.add(relay.region)
    if (typeof relay?.diskGB === 'number' && Number.isFinite(relay.diskGB) && relay.diskGB >= 0) {
      declaredNominalDiskGB += relay.diskGB
    }
  }
  return {
    relayCount: relays.length,
    channels,
    regionCount: regions.size,
    declaredNominalDiskGB,
    declaredDiskIsObservedCapacityEvidence: false
  }
}

function summarizeProjections (input) {
  return {
    retentionCensusDigest: validDigest(input.retentionCensus?.evidenceDigest),
    uniqueUnknownObjects: nonNegativeInteger(input.retentionCensus?.uniqueUnknownObjects)
      ? input.retentionCensus.uniqueUnknownObjects
      : null,
    capacityEvidenceDigest: validDigest(input.capacityProjection?.evidenceDigest),
    afterDrainFreeBytes: nonNegativeInteger(input.capacityProjection?.afterDrainFreeBytes)
      ? input.capacityProjection.afterDrainFreeBytes
      : null,
    minimumFreeBytes: nonNegativeInteger(input.capacityProjection?.minimumFreeBytes)
      ? input.capacityProjection.minimumFreeBytes
      : null,
    replicaEvidenceDigest: validDigest(input.replicaProjection?.evidenceDigest),
    afterDrainMinimumByFamily: isObject(input.replicaProjection?.afterDrainMinimumByFamily)
      ? input.replicaProjection.afterDrainMinimumByFamily
      : null,
    floorByFamily: isObject(input.replicaProjection?.floorByFamily)
      ? input.replicaProjection.floorByFamily
      : null,
    restoreEvidenceDigest: validDigest(input.restore?.evidenceDigest),
    rollbackEvidenceDigest: validDigest(input.rollback?.evidenceDigest)
  }
}

function proposedSequence (targetRelay) {
  const target = targetRelay || '<one-explicit-relay>'
  return [
    { order: 1, action: 'freeze-inputs', guard: 'independently accept exact source, artifact, contract hashes, and signed inventory' },
    { order: 2, action: 'stop-on-unknown-custody', guard: 'uniqueUnknownObjects must equal zero' },
    { order: 3, action: 'prove-restore', guard: 'off-node backup must pass an isolated restore before drain' },
    { order: 4, action: `project-drain:${target}`, guard: 'capacity and per-family replica floors must remain satisfied without the target' },
    { order: 5, action: `drain-one:${target}`, guard: 'requires a separate explicit human fleet-operation lease' },
    { order: 6, action: 'provision-distinct-empty-root', guard: 'run only the exact accepted artifact; preserve only explicitly approved identities' },
    { order: 7, action: 'restore-or-republish', guard: 'T1 from authoritative publishers; blind data only through receipts and client-owned state' },
    { order: 8, action: 'verify-convergence', guard: 'heads, proofs, capacity, clocks, quotas, storage generation, and contract hashes must match' },
    { order: 9, action: 'observe-and-soak', guard: 'do not advance another node before the required windows pass' },
    { order: 10, action: 'retain-old-root', guard: 'D-7 allows only the exact dual-read rollback artifact; never target a legacy-only writer' }
  ]
}

function validTargetContracts (value) {
  return isObject(value) &&
    DIGEST.test(value.specHash || '') &&
    DIGEST.test(value.storeFormatHash || '') &&
    DIGEST.test(value.privateIpcV2Hash || '')
}

function validTimestamp (value) {
  return nonEmpty(value) && Number.isFinite(Date.parse(value))
}

function validDigest (value) {
  return DIGEST.test(value || '') ? value : null
}

function duplicateValues (values) {
  const seen = new Set()
  const duplicates = new Set()
  for (const value of values) {
    if (seen.has(value)) duplicates.add(value)
    seen.add(value)
  }
  return [...duplicates].sort()
}

function positiveInteger (value) {
  return Number.isSafeInteger(value) && value > 0
}

function nonNegativeInteger (value) {
  return Number.isSafeInteger(value) && value >= 0
}

function nonEmpty (value) {
  return typeof value === 'string' && value.length > 0
}

function isObject (value) {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
}

function stableStringify (value) {
  return JSON.stringify(sortValue(value))
}

function sortValue (value) {
  if (Array.isArray(value)) return value.map(sortValue)
  if (!isObject(value)) return value
  return Object.fromEntries(Object.keys(value).sort().map(key => [key, sortValue(value[key])]))
}

function sha256 (value) {
  return createHash('sha256').update(value).digest('hex')
}

function usage () {
  return [
    'Usage: node fleet/plan-reprovision.mjs [options]',
    '',
    'Options:',
    '  --inventory FILE        Fleet inventory (default: fleet/relays.json)',
    '  --state FILE            Local reprovision evidence/input document',
    '  --target-relay NAME     Exactly one relay to project',
    '  --source-commit SHA     Exact source commit override',
    '  --artifact-sha256 SHA   Exact immutable release artifact override',
    '  --release-id ID         Immutable release identity override',
    '  --release-sequence N    Monotonic release sequence override',
    '  --as-of TIMESTAMP        Timestamp used for inventory freshness',
    '  --max-inventory-age N    Maximum accepted inventory age in seconds',
    '  --out FILE              Write the report locally instead of stdout',
    '  --pretty | --compact    JSON formatting',
    '  --require-ready         Exit 2 when local evidence is incomplete',
    '',
    'There is deliberately no execute, SSH, deploy, channel, key, or root-mutation mode.'
  ].join('\n')
}

async function main (argv) {
  const args = parseArgs(argv)
  if (args.help) {
    process.stdout.write(`${usage()}\n`)
    return
  }
  const inventory = JSON.parse(await readFile(resolve(args.inventory || 'fleet/relays.json'), 'utf8'))
  const state = args.state
    ? JSON.parse(await readFile(resolve(args.state), 'utf8'))
    : {}
  const input = {
    ...state,
    schema: state.schema || REPROVISION_INPUT_SCHEMA,
    inventory,
    targetRelay: args.targetRelay || state.targetRelay,
    evaluatedAt: args.asOf || state.evaluatedAt || new Date().toISOString(),
    maximumInventoryAgeSeconds: args.maxInventoryAge
      ? Number(args.maxInventoryAge)
      : state.maximumInventoryAgeSeconds,
    artifact: {
      ...(isObject(state.artifact) ? state.artifact : {}),
      ...(args.sourceCommit ? { sourceCommit: args.sourceCommit } : {}),
      ...(args.artifactSha256 ? { sha256: args.artifactSha256 } : {}),
      ...(args.releaseId ? { releaseId: args.releaseId } : {}),
      ...(args.releaseSequence ? { releaseSequence: Number(args.releaseSequence) } : {})
    }
  }
  const report = buildFleetReprovisionPlan(input)
  const output = `${JSON.stringify(report, null, args.pretty ? 2 : 0)}\n`
  if (args.out) await writeFile(resolve(args.out), output)
  else process.stdout.write(output)
  if (args.requireReady && report.status !== 'ready-for-operator-review') process.exitCode = 2
}

function parseArgs (argv) {
  const result = { pretty: true, requireReady: false, help: false }
  const valued = new Map([
    ['--inventory', 'inventory'],
    ['--state', 'state'],
    ['--target-relay', 'targetRelay'],
    ['--source-commit', 'sourceCommit'],
    ['--artifact-sha256', 'artifactSha256'],
    ['--release-id', 'releaseId'],
    ['--release-sequence', 'releaseSequence'],
    ['--as-of', 'asOf'],
    ['--max-inventory-age', 'maxInventoryAge'],
    ['--out', 'out']
  ])
  for (let index = 0; index < argv.length; index++) {
    const arg = argv[index]
    if (valued.has(arg)) {
      const value = argv[++index]
      if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`)
      result[valued.get(arg)] = value
    } else if (arg === '--pretty') result.pretty = true
    else if (arg === '--compact') result.pretty = false
    else if (arg === '--require-ready') result.requireReady = true
    else if (arg === '--help' || arg === '-h') result.help = true
    else throw new Error(`unknown argument ${arg}`)
  }
  return result
}

if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
  main(process.argv.slice(2)).catch(error => {
    process.stderr.write(`fleet reprovision planner: ${error.message}\n`)
    process.exitCode = 1
  })
}