#!/bin/sh # SPDX-License-Identifier: MIT # NumericalOS control floor (Spec 1). # # Implements: identity-init, capability, join, receipt-export, run-op (dispatch edge only). # Does not implement: /run/numinit.sock, numinit query API, op interpreter. # Unimplemented commands halt with a named reason (docs/PRINCIPLES.md). set -eu numctl_die() { echo "numctl: HALT: $*" >&2 exit 1 } cmd="${1:-}" [ -n "$cmd" ] || numctl_die "usage: numctl [args]" shift # Same default as numinit: a development machine cannot create /run/numos, # so tests and host installs set NUMOS_RUNDIR. RUNDIR="${NUMOS_RUNDIR:-/run/numos}" numctl_identity_init() { mkdir -p "$RUNDIR" || numctl_die "cannot create $RUNDIR" # Idempotent: a second boot must not rotate identity. if [ -f "$RUNDIR/identity" ]; then return 0 fi # Floor identity is host-local, not a cryptographic claim and not a beacon. # Prefer kernel entropy when present; fall back to a stable-enough stamp. if [ -r /dev/urandom ] && command -v od >/dev/null 2>&1; then id="$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')" else id="$(uname -n 2>/dev/null || echo unknown)-$$-$(date +%s 2>/dev/null || echo 0)" fi [ -n "$id" ] || numctl_die "could not generate identity" # Write via temp + rename so a crashed init never leaves a half file that # a later boot treats as authoritative. tmp="$RUNDIR/identity.part" echo "$id" > "$tmp" || numctl_die "cannot write $tmp" mv "$tmp" "$RUNDIR/identity" || numctl_die "cannot install identity" } # ---------------------------------------------------------------- capability # # What this node would advertise. Written to disk BEFORE anything is sent, so # an operator can read exactly what would leave the machine. # # Every field is measured or reported as `-`. Nothing is guessed: a node that # cannot count its own cores says so rather than claiming a number, because a # capability document is a claim a coordinator will schedule against. numctl_capability() { mkdir -p "$RUNDIR" || numctl_die "cannot create $RUNDIR" raw="$(uname -m 2>/dev/null || echo unknown)" arch="$raw" if [ -n "${NUMOS_LIB:-}" ] && [ -r "$NUMOS_LIB/arch_table.sh" ]; then . "$NUMOS_LIB/arch_table.sh" canon="$(numos_canonical_arch "$raw" 2>/dev/null)" && arch="$canon" fi cores="-" if command -v nproc >/dev/null 2>&1; then cores="$(nproc 2>/dev/null)" || cores="-" elif [ -r /proc/cpuinfo ]; then cores="$(grep -c '^processor' /proc/cpuinfo 2>/dev/null)" || cores="-" fi case "$cores" in ""|*[!0-9]*) cores="-" ;; esac ram_mb="-" if [ -r /proc/meminfo ]; then kb="$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null)" case "$kb" in ""|*[!0-9]*) ram_mb="-" ;; *) ram_mb=$((kb / 1024)) ;; esac fi degraded=0 [ -f "$RUNDIR/degraded" ] && degraded=1 tmp="$RUNDIR/capability.part" { echo "V 1" echo "arch $arch" echo "cores $cores" echo "ram_mb $ram_mb" echo "degraded $degraded" # Supported ops come from the state this node actually booted, not from a # registry it was told about. An op unit that is not in the state is not a # capability of this machine. if [ -n "${NUMOS_STATE:-}" ] && [ -r "$NUMOS_STATE" ]; then # `{ ... } > file || die` checks the status of the TRAILING command, so # a grep failure inside a pipeline was swallowed and an empty `while` # still exited 0 -- capability installed "successfully" while silently # under-reporting ops_supported. Same "status of the inner thing is not # the status checked" class this codebase has been bitten by before, so # grep runs on its own first and its status is the one inspected. # grep exits 1 on no-match, which is a legitimate empty catalogue; only # 2-and-above is a real read error. oplines="$(grep '^U op-' "$NUMOS_STATE")" || [ $? -eq 1 ] || numctl_die "cannot read ops from $NUMOS_STATE" if [ -n "$oplines" ]; then echo "$oplines" | cut -d' ' -f2 | sort | while read -r opname; do [ -n "$opname" ] && echo "op $opname" done fi fi } > "$tmp" || numctl_die "cannot write $tmp" mv "$tmp" "$RUNDIR/capability" || numctl_die "cannot install capability" echo "$RUNDIR/capability" } # ---------------------------------------------------------------- join # # Reconciling this with the no-telemetry commitment (docs/PRINCIPLES.md, # CCC 1907). A join IS an outbound signal, so the distinction has to be # principled rather than convenient: # # telemetry the PROJECT harvesting from the OPERATOR's machine # a join the OPERATOR's machine reporting to the OPERATOR's coordinator # # What keeps this on the right side of that line, and all four are load-bearing: # # 1. OFF BY DEFAULT. No NUMOS_COORDINATOR means no network contact, ever. # The capability is still written to disk, so the node is observable # without anything leaving it. # 2. NEVER to this project. The destination is whatever the operator named. # numericalos.com is not a fallback and is not consulted. # 3. INSPECTABLE FIRST. The exact bytes are on disk before any send. # 4. REFUSAL IS A STATE, not an error. `disabled` is a normal outcome. # # And the property this whole contract exists for: a join that fails is # recorded as FAILED. Never as joined. The reason the topology claim was # unfalsifiable is that a machine which never joined looked identical to one # that did; writing an honest outcome is what closes that. numctl_join() { mkdir -p "$RUNDIR" || numctl_die "cannot create $RUNDIR" capfile="$(numctl_capability)" if [ -z "${NUMOS_COORDINATOR:-}" ]; then echo "disabled" > "$RUNDIR/join.state" echo "numctl: join disabled (no NUMOS_COORDINATOR); capability at $capfile" >&2 else echo "pending" > "$RUNDIR/join.state" if numctl_post "$NUMOS_COORDINATOR" "$capfile"; then echo "joined $NUMOS_COORDINATOR" > "$RUNDIR/join.state" echo "numctl: joined $NUMOS_COORDINATOR" >&2 else # Deliberately not fatal: a coordinator being unreachable must not stop # a machine supervising itself (subsidiarity, CCC 1883). It must also # not be silent. echo "failed $NUMOS_COORDINATOR" > "$RUNDIR/join.state" echo "numctl: join FAILED against $NUMOS_COORDINATOR; node runs unjoined" >&2 fi fi # Longrun: stay alive so phase 50 and steady state have a real PID. while :; do sleep 3600 2>/dev/null || sleep 60 done } numctl_post() { url="$1" file="$2" if command -v curl >/dev/null 2>&1; then curl -fsS --max-time 30 -X POST -H 'Content-Type: text/plain' --data-binary "@$file" "$url" >/dev/null 2>&1 elif command -v wget >/dev/null 2>&1; then wget -q -O /dev/null --timeout=30 --post-file="$file" "$url" 2>/dev/null else return 1 fi } # ---------------------------------------------------------------- run-op # # Dispatch one unit of work, and record what actually happened. # # Two refusals are load-bearing, and neither is a placeholder: # # 1. An op this node does not declare is REFUSED. `numctl capability` # advertises ops_supported straight from the U records in this node's # own state, so running something outside that set would mean acting # beyond what was advertised - the node would be lying about itself in # the one direction a coordinator trusts. # 2. With no NUMOS_OP_RUNTIME configured, dispatch is `unavailable` and # exits nonzero. It does not succeed quietly. A node that cannot do work # must not look like one that did it, which is the same byte-identity # problem the join contract exists to close. # # What this is NOT: an op interpreter. NumericalOS does not implement the ops # themselves - they live in the topology runtime. This is the dispatch edge # and the record of its outcome, nothing more. numctl_run_op() { op="$1" mkdir -p "$RUNDIR/ops" || numctl_die "cannot create $RUNDIR/ops" # Unit names are the op name lowercased with underscores as dashes. suffix="$(echo "$op" | tr 'A-Z_' 'a-z-')" unit="op-$suffix" if [ -z "${NUMOS_STATE:-}" ] || [ ! -r "$NUMOS_STATE" ]; then numctl_die "run-op $op: no readable NUMOS_STATE; cannot confirm this node declares it" fi if ! grep -q "^U $unit " "$NUMOS_STATE" 2>/dev/null; then numctl_die "run-op $op: not declared by this node (no unit $unit); a node must not run what it does not advertise" fi statefile="$RUNDIR/ops/$unit.state" if [ -z "${NUMOS_OP_RUNTIME:-}" ]; then echo "unavailable" > "$statefile" numctl_die "run-op $op: no NUMOS_OP_RUNTIME configured; dispatch is unavailable on this node" fi echo "dispatching" > "$statefile" # A node asking for work identifies itself with the same document it would # advertise. Build it if this is the first request since boot -- posting a # file that does not exist would fail the dispatch for a reason that has # nothing to do with the runtime. [ -f "$RUNDIR/capability" ] || numctl_capability >/dev/null if numctl_post "$NUMOS_OP_RUNTIME/$op" "$RUNDIR/capability" 2>/dev/null; then echo "ok $op" > "$statefile" echo "numctl: run-op $op dispatched" >&2 return 0 fi echo "failed $op" > "$statefile" numctl_die "run-op $op: dispatch failed against $NUMOS_OP_RUNTIME" } # Algoblocker privacy receipt. Disk only. Never POSTs. NUMOS_COORDINATOR is # ignored here — a join is a different command. Operator stamps values via # env; defaults are the unconfigured 2+1 example. numctl_receipt_export() { mkdir -p "$RUNDIR" || numctl_die "cannot create $RUNDIR" ext="${NUMOS_ALGO_EXT:-0.1.1}" vpn="${NUMOS_ALGO_VPN:-unconfigured}" ks="${NUMOS_ALGO_KILLSWITCH:-off}" res="${NUMOS_ALGO_RESOLVER:-off}" case "$ext$vpn$ks$res" in *@*) numctl_die "receipt-export: identifying @ in unit fields" ;; esac for _f in "$ext" "$vpn" "$ks" "$res"; do case "$_f" in *[0-9].[0-9]*.[0-9]*.[0-9]*) numctl_die "receipt-export: identifying ipv4 in unit fields" ;; esac done body="$RUNDIR/algoblocker-privacy-posture.body" out="$RUNDIR/algoblocker-privacy-posture.txt" { printf 'V 1\n' printf 'K algoblocker-privacy-posture\n' printf 'U extension %s present\n' "$ext" printf 'U vpn %s\n' "$vpn" printf 'U killswitch %s\n' "$ks" printf 'U resolver %s\n' "$res" if [ "${NUMOS_ALGO_WORKSTATION:-}" = "present" ]; then printf 'U workstation present\n' fi } > "$body" || numctl_die "cannot write $body" if ! command -v sha256sum >/dev/null 2>&1; then numctl_die "receipt-export: sha256sum not on PATH (need numos-floor)" fi digest="$(sha256sum "$body" | cut -d' ' -f1)" case "$digest" in *[!0-9a-f]*|"") numctl_die "receipt-export: bad digest" ;; esac { printf 'V 1\nC %s\n' "$digest"; sed '1d' "$body"; } > "$out" || numctl_die "cannot write $out" echo "numctl: receipt-export wrote $out" >&2 } case "$cmd" in identity-init) numctl_identity_init ;; join) numctl_join ;; capability) numctl_capability ;; receipt-export) numctl_receipt_export ;; status) numctl_die "control socket not implemented; cannot query a running instance" ;; run-op) op="${1:-}" [ -n "$op" ] || numctl_die "usage: numctl run-op " numctl_run_op "$op" ;; *) numctl_die "unknown command: $cmd" ;; esac