← Files NightshiftARCHIVED FILE

lib/rules-read.awk

32.4 KB · Oct 2, 2026 · 00:30 UTC

↓ Download file

# Strict-subset scanner for Nightshift rules.json. Not a general JSON parser.
# Allowed: the template's objects, strings, bools, integers, and string arrays.
# Anything else fails closed with a named reason.
#
# Modes:
#   rules    full rules.json shape
#   strings  one object of string values (toolDeny / session override)
#   policy   the shift policy snapshot, as the flat fact stream lib/policy.sh reads
#   defaults the remembered composition choices, as the same kind of fact stream
#   canon    one JSON document as compact canonical JSON: sorted keys, one line
#   pretty   the same document sorted and indented, for a file the owner reads
#   setblock the same, with one top-level key set to -v value; every other key survives
#   canonat  the compact canonical JSON of the value at dotted path -v key, or nothing when absent
#   renamekey the document with top-level key -v key renamed to -v value, sorted and indented
#   dropkey  the document without the value at dotted path -v key, sorted and indented
#   unquote  one JSON string on stdin, decoded text on stdout

{
  src = src $0
  if (mode != "unquote") {
    src = src "\n"
  }
}

END {
  if (mode == "unquote") {
    if (unquote_one(src) == 0) {
      printf "%s", decoded
    }
    exit
  }
  n = length(src)
  i = 1
  skip_ws()
  if (i > n) {
    fail("empty document")
  }
  if (mode == "canon" || mode == "pretty" || mode == "setblock" || mode == "canonat" ||
      mode == "renamekey" || mode == "dropkey") {
    jparse(".")
    skip_ws()
    if (i <= n) {
      fail("unexpected nesting")
    }
    if (mode == "canonat") {
      if (key in V_TYPE) {
        printf "%s\n", canon(key)
      }
      exit
    }
    if (mode == "renamekey") {
      renamekey(key, value)
      printf "%s\n", pretty(".", "")
      exit
    }
    if (mode == "dropkey") {
      dropkey(key)
      printf "%s\n", pretty(".", "")
      exit
    }
    if (mode == "setblock") {
      if (key == "") {
        fail("setblock needs a key")
      }
      setblock(key, value)
      printf "%s\n", pretty(".", "")
    } else if (mode == "pretty") {
      printf "%s\n", pretty(".", "")
    } else {
      printf "%s\n", canon(".")
    }
    exit
  }
  if (substr(src, i, 1) != "{") {
    # A document that is valid JSON but not an object is a fact the caller states,
    # not a read that failed: policy.sh names the field it wanted.
    if (mode == "policy" || mode == "defaults") {
      printf "x\t.\tnotobject\n"
      exit
    }
    fail("not a JSON object")
  }
  if (mode == "strings") {
    parse_string_object()
  } else if (mode == "policy" || mode == "defaults") {
    jparse(".")
  } else {
    parse_rules_object()
  }
  skip_ws()
  if (i <= n) {
    fail("unexpected nesting")
  }
  if (mode == "policy") {
    emit_policy()
  } else if (mode == "defaults") {
    emit_defaults()
  }
  printf "%s", outbuf
}

# ------------------------------------------------------------------ policy mode
#
# One shift policy snapshot as the same flat fact stream lib/policy.sh already reads from jq or
# python3, so an owner choice recorded for the night survives a host with neither. The document
# is parsed into paths first: a fact stream names absent keys as well as present ones, and only a
# finished parse knows which are which. Nothing here decides anything - policy.sh still owns
# validation, precedence and matching.

function jparse(path,    c, key, idx, first, keys, child) {
  skip_ws()
  c = peek()
  if (c == "{") {
    V_TYPE[path] = "object"
    i++
    skip_ws()
    if (peek() == "}") {
      i++
      V_KEYS[path] = ""
      return
    }
    keys = ""
    first = 1
    while (1) {
      skip_ws()
      if (peek() != "\"") {
        fail("not a JSON object")
      }
      parse_string_raw()
      key = decoded
      if (keys != "") {
        keys = keys "\t"
      }
      keys = keys scrub(key)
      skip_ws()
      if (peek() != ":") {
        fail("truncated JSON")
      }
      i++
      if (path == ".") {
        child = key
      } else {
        child = path "." key
      }
      jparse(child)
      first = 0
      skip_ws()
      c = peek()
      if (c == ",") {
        i++
        skip_ws()
        if (peek() == "}") {
          fail("trailing comma")
        }
        continue
      }
      if (c == "}") {
        i++
        V_KEYS[path] = keys
        return
      }
      fail("truncated JSON")
    }
  }
  if (c == "[") {
    V_TYPE[path] = "array"
    i++
    skip_ws()
    if (peek() == "]") {
      i++
      V_LEN[path] = 0
      return
    }
    idx = 0
    while (1) {
      jparse(path "[" idx "]")
      idx++
      skip_ws()
      c = peek()
      if (c == ",") {
        i++
        skip_ws()
        if (peek() == "]") {
          fail("trailing comma")
        }
        continue
      }
      if (c == "]") {
        i++
        V_LEN[path] = idx
        return
      }
      fail("truncated JSON")
    }
  }
  if (c == "\"") {
    parse_string_raw()
    V_TYPE[path] = "string"
    V_STR[path] = decoded
    V_RAW[path] = jenc(decoded)
    return
  }
  if (c == "t" || c == "f") {
    V_TYPE[path] = "boolean"
    V_RAW[path] = parse_bool()
    return
  }
  if (c == "n") {
    parse_null()
    V_TYPE[path] = "null"
    V_RAW[path] = "null"
    return
  }
  if (c == "-" || (c >= "0" && c <= "9")) {
    V_TYPE[path] = "number"
    V_RAW[path] = parse_signed()
    return
  }
  fail("unknown type")
}

function parse_signed(    start) {
  start = i
  if (peek() == "-") {
    i++
  }
  return substr(src, start, i - start) parse_number()
}

# A fact line holds exactly one fact, so a control character never travels in one.
function scrub(s,    j, c, o) {
  o = ""
  for (j = 1; j <= length(s); j++) {
    c = substr(s, j, 1)
    if (c < " " || c == "\177") {
      o = o " "
    } else {
      o = o c
    }
  }
  return o
}

# Whitespace runs collapse and the ends are trimmed, so one approved command compares equal
# however the document happened to wrap it.
function norm(s,    o, c, j, sp) {
  o = ""
  sp = 0
  for (j = 1; j <= length(s); j++) {
    c = substr(s, j, 1)
    if (c == " " || c == "\t" || c == "\n" || c == "\r" || c == "\013" || c == "\014") {
      sp = 1
      continue
    }
    if (sp && o != "") {
      o = o " "
    }
    sp = 0
    o = o c
  }
  return o
}

function ptype(path) {
  if (path in V_TYPE) {
    return V_TYPE[path]
  }
  return "null"
}

function praw(path) {
  if (path in V_RAW) {
    return V_RAW[path]
  }
  return "null"
}

function plen(path) {
  if (ptype(path) == "array") {
    return V_LEN[path] + 0
  }
  return -1
}

function put(line) {
  outbuf = outbuf line "\n"
}

function emit_keys(path, label,    parts, count, j) {
  if (ptype(path) != "object") {
    return
  }
  count = split(V_KEYS[path], parts, "\t")
  for (j = 1; j <= count; j++) {
    if (parts[j] == "" && V_KEYS[path] == "") {
      continue
    }
    put("k\t" label "\t" parts[j])
  }
}

function emit_scalars(path, label, names,    parts, count, j, full) {
  count = split(names, parts, " ")
  for (j = 1; j <= count; j++) {
    if (label == ".") {
      full = parts[j]
    } else {
      full = label "." parts[j]
    }
    if (path == ".") {
      put("j\t" full "\t" praw(parts[j]))
    } else {
      put("j\t" full "\t" praw(path "." parts[j]))
    }
  }
}

# One line per remembered choice: whether the file states it, and what it states.
function emit_defaults(    parts, count, j, k) {
  if (ptype(".") != "object") {
    put("x\t.\tnotobject")
    return
  }
  count = split("schemaVersion verificationProfile hours toolingPolicy execution updatedAt", parts, " ")
  for (j = 1; j <= count; j++) {
    k = parts[j]
    if (k in V_TYPE) {
      put("d\t" k "\t1\t" praw(k))
    } else {
      put("d\t" k "\t0\tnull")
    }
  }
}

# praw_pref(path) — one preference value as compact JSON. An array is rebuilt from its indexed
# elements, because the reader keeps those and not the text they came from; every other type is
# already raw. This is the awk half of the two canonical writers and has to match them byte for
# byte, so a list of strings prints exactly as ["a","b"].
function praw_pref(path,    n, i, out, kind) {
  kind = ptype(path)
  if (kind != "array") {
    return praw(path)
  }
  n = plen(path)
  out = ""
  for (i = 0; i < n; i++) {
    if (i > 0) {
      out = out ","
    }
    out = out praw(path "[" i "]")
  }
  return "[" out "]"
}

# The owner preference blocks tonight's snapshot freezes. A block the snapshot does not carry
# emits its type and no fields, which is how a policy written before this feature is told apart
# from one whose owner left a block empty.
function emit_preferences(    blocks, fields, count, j, k, parts, fcount) {
  count = split("shift recovery handoff archive receipts", blocks, " ")
  fields["shift"] = "execution hours toolingPolicy verificationProfile"
  fields["recovery"] = "launchScope"
  fields["handoff"] = "detail enabled language sections templatePath view"
  fields["archive"] = "automatic layout root templatePath"
  fields["receipts"] = "duration enabled progressMinutes progressMode progressTokens templatePath usage"
  for (j = 1; j <= count; j++) {
    put("ty\t" blocks[j] "\t" ptype(blocks[j]))
    if (!(blocks[j] in V_TYPE)) {
      continue
    }
    fcount = split(fields[blocks[j]], parts, " ")
    for (k = 1; k <= fcount; k++) {
      put("j\t" blocks[j] "." parts[k] "\t" praw_pref(blocks[j] "." parts[k]))
    }
  }
}

function emit_policy(    j, k, ap, cs, ws, cnt, val) {
  if (ptype(".") != "object") {
    put("x\t.\tnotobject")
    return
  }
  emit_keys(".", ".")
  emit_scalars(".", ".", "schemaVersion shiftId createdAt source deadlineEpoch verificationLevel toolingPolicy launchScope launchProvenance completionMode gatesDigest contractDigest itemsDigest")
  emit_preferences()

  put("ty\tbudgets\t" ptype("budgets"))
  if (ptype("budgets") == "object") {
    cnt = split(V_KEYS["budgets"], BK, "\t")
    for (j = 1; j <= cnt; j++) {
      if (BK[j] == "" && V_KEYS["budgets"] == "") {
        continue
      }
      put("b\t" BK[j] "\t" praw("budgets." BK[j]))
    }
  }

  put("ty\tselectedDebt\t" ptype("selectedDebt"))
  put("n\tselectedDebt\t" plen("selectedDebt"))
  for (j = 0; j < plen("selectedDebt"); j++) {
    if (ptype("selectedDebt[" j "]") == "string") {
      put("s\t" j "\ts\t" praw("selectedDebt[" j "]"))
    } else {
      put("s\t" j "\tx\t" praw("selectedDebt[" j "]"))
    }
  }

  put("ty\tallowances\t" ptype("allowances"))
  put("n\tallowances\t" plen("allowances"))
  for (j = 0; j < plen("allowances"); j++) {
    ap = "allowances[" j "]"
    put("ty\t" ap "\t" ptype(ap))
    emit_keys(ap, ap)
    emit_scalars(ap, ap, "category scope provenance")
    put("ty\t" ap ".plan\t" ptype(ap ".plan"))
    emit_keys(ap ".plan", ap ".plan")
    emit_scalars(ap ".plan", ap ".plan", "workTarget digest expiry")
    cs = ap ".plan.commands"
    put("ty\t" cs "\t" ptype(cs))
    put("n\t" cs "\t" plen(cs))
    for (k = 0; k < plen(cs); k++) {
      if (ptype(cs "[" k "]") == "string") {
        val = norm(V_STR[cs "[" k "]"])
        put("c\t" j "\t" k "\ts\t" scrub(val))
        put("q\t" j "\t" k "\t" jenc(val))
      } else {
        put("c\t" j "\t" k "\tx\t")
        put("q\t" j "\t" k "\tnull")
      }
    }
    ws = ap ".plan.writeSurface"
    put("ty\t" ws "\t" ptype(ws))
    for (k = 0; k < plen(ws); k++) {
      if (ptype(ws "[" k "]") == "string") {
        put("w\t" j "\t" k "\ts")
      } else {
        put("w\t" j "\t" k "\tx")
      }
    }
  }
}

# canon(path) — the parsed value at path as compact canonical JSON. Keys sort in byte order,
# which is what every other canonical writer here produces, so one document has one form
# whichever reader wrote it.
function canon(path,    t, out, keys, count, j, child, first) {
  t = ptype(path)
  if (t == "object") {
    count = split(V_KEYS[path], keys, "\t")
    if (V_KEYS[path] == "") {
      count = 0
    }
    sort_keys(keys, count)
    out = "{"
    first = 1
    for (j = 1; j <= count; j++) {
      if (path == ".") {
        child = keys[j]
      } else {
        child = path "." keys[j]
      }
      if (!first) {
        out = out ","
      }
      first = 0
      out = out cenc(keys[j]) ":" canon(child)
    }
    return out "}"
  }
  if (t == "array") {
    out = "["
    for (j = 0; j < V_LEN[path]; j++) {
      if (j > 0) {
        out = out ","
      }
      out = out canon(path "[" j "]")
    }
    return out "]"
  }
  if (t == "string") {
    return cenc(V_STR[path])
  }
  return praw(path)
}

function sort_keys(a, count,    j, k, tmp) {
  for (j = 2; j <= count; j++) {
    tmp = a[j]
    k = j - 1
    while (k >= 1 && a[k] > tmp) {
      a[k + 1] = a[k]
      k--
    }
    a[k + 1] = tmp
  }
}

# The canonical string form: the short escapes where JSON has one, and \uXXXX for every control
# character and every codepoint above ASCII, so one document has one form on any host. Bytes are
# read as UTF-8 here, which is why the callers run this under LC_ALL=C.
function cenc(s,    j, c, o, b, cb, code, len, k, ok, hi, lo) {
  o = "\""
  j = 1
  while (j <= length(s)) {
    c = substr(s, j, 1)
    b = ord(c)
    if (c == "\\") {
      o = o "\\\\"
      j++
      continue
    }
    if (c == "\"") {
      o = o "\\\""
      j++
      continue
    }
    if (c == "\n") {
      o = o "\\n"
      j++
      continue
    }
    if (c == "\r") {
      o = o "\\r"
      j++
      continue
    }
    if (c == "\t") {
      o = o "\\t"
      j++
      continue
    }
    if (c == "\b") {
      o = o "\\b"
      j++
      continue
    }
    if (c == "\f") {
      o = o "\\f"
      j++
      continue
    }
    if (b < 32) {
      o = o sprintf("\\u%04x", b)
      j++
      continue
    }
    if (b < 128) {
      o = o c
      j++
      continue
    }
    if (b >= 240) {
      len = 4
      code = b - 240
    } else if (b >= 224) {
      len = 3
      code = b - 224
    } else if (b >= 192) {
      len = 2
      code = b - 192
    } else {
      o = o c
      j++
      continue
    }
    ok = 1
    for (k = 1; k < len; k++) {
      cb = ord(substr(s, j + k, 1))
      if (cb < 128 || cb > 191) {
        ok = 0
        break
      }
      code = code * 64 + (cb - 128)
    }
    if (!ok) {
      o = o c
      j++
      continue
    }
    j += len
    if (code > 65535) {
      code -= 65536
      hi = 55296 + int(code / 1024)
      lo = 56320 + (code % 1024)
      o = o sprintf("\\u%04x\\u%04x", hi, lo)
    } else {
      o = o sprintf("\\u%04x", code)
    }
  }
  return o "\""
}

# pretty(path, indent) — the same sorted document a person reads: two spaces a level, one entry
# a line, and a string left as the UTF-8 it arrived as.
# setblock(k, text) — replace one top-level key with the document `text` holds, adding the key
# when the file does not carry it. Every other key keeps its own value, including one this
# version has never heard of: a plugin update fills settings in, it never takes them away.
function setblock(k, text,    save_src, save_n, save_i, keys, count, j, found) {
  drop_subtree(k)
  save_src = src
  save_n = n
  save_i = i
  src = text
  n = length(src)
  i = 1
  jparse(k)
  skip_ws()
  if (i <= n) {
    fail("the replacement value is not one JSON document")
  }
  src = save_src
  n = save_n
  i = save_i
  count = split(V_KEYS["."], keys, "\t")
  if (V_KEYS["."] == "") {
    count = 0
  }
  found = 0
  for (j = 1; j <= count; j++) {
    if (keys[j] == k) {
      found = 1
    }
  }
  if (!found) {
    if (V_KEYS["."] == "") {
      V_KEYS["."] = k
    } else {
      V_KEYS["."] = V_KEYS["."] "\t" k
    }
  }
}

# A top-level key under a new name, with everything it held. An absent key changes nothing.
function renamekey(from, to,    text) {
  if (!(from in V_TYPE)) {
    return
  }
  text = canon(from)
  dropkey(from)
  setblock(to, text)
}

# The value at a dotted path, and its name in the object that held it, are gone. An absent path
# changes nothing.
function dropkey(path,    parent, leaf, cut, keys, count, j, kept) {
  if (!(path in V_TYPE)) {
    return
  }
  cut = 0
  for (j = length(path); j > 0; j--) {
    if (substr(path, j, 1) == ".") {
      cut = j
      break
    }
  }
  if (cut == 0) {
    parent = "."
    leaf = path
  } else {
    parent = substr(path, 1, cut - 1)
    leaf = substr(path, cut + 1)
  }
  count = split(V_KEYS[parent], keys, "\t")
  if (V_KEYS[parent] == "") {
    count = 0
  }
  kept = ""
  for (j = 1; j <= count; j++) {
    if (keys[j] == leaf) {
      continue
    }
    kept = (kept == "" ? keys[j] : kept "\t" keys[j])
  }
  V_KEYS[parent] = kept
  drop_subtree(path)
}

# Everything the old value owned goes with it, so a shorter replacement cannot leave a field
# of the previous one behind.
function drop_subtree(k,    path) {
  for (path in V_TYPE) {
    if (path == k || substr(path, 1, length(k) + 1) == k "." ||
        substr(path, 1, length(k) + 1) == k "[") {
      delete V_TYPE[path]
      delete V_RAW[path]
      delete V_STR[path]
      delete V_KEYS[path]
      delete V_LEN[path]
    }
  }
}

function pretty(path, pad,    t, out, keys, count, j, child, inner) {
  t = ptype(path)
  inner = pad "  "
  if (t == "object") {
    count = split(V_KEYS[path], keys, "\t")
    if (V_KEYS[path] == "") {
      count = 0
    }
    if (count == 0) {
      return "{}"
    }
    sort_keys(keys, count)
    out = "{\n"
    for (j = 1; j <= count; j++) {
      if (path == ".") {
        child = keys[j]
      } else {
        child = path "." keys[j]
      }
      out = out inner penc(keys[j]) ": " pretty(child, inner)
      if (j < count) {
        out = out ","
      }
      out = out "\n"
    }
    return out pad "}"
  }
  if (t == "array") {
    if (V_LEN[path] == 0) {
      return "[]"
    }
    out = "[\n"
    for (j = 0; j < V_LEN[path]; j++) {
      out = out inner pretty(path "[" j "]", inner)
      if (j < V_LEN[path] - 1) {
        out = out ","
      }
      out = out "\n"
    }
    return out pad "]"
  }
  if (t == "string") {
    return penc(V_STR[path])
  }
  return praw(path)
}

# The readable string form: the short escapes JSON defines and \uXXXX for a control character,
# with every printable byte left as it stands.
function penc(s,    j, c, o, b) {
  o = "\""
  for (j = 1; j <= length(s); j++) {
    c = substr(s, j, 1)
    b = ord(c)
    if (c == "\\") {
      o = o "\\\\"
    } else if (c == "\"") {
      o = o "\\\""
    } else if (c == "\n") {
      o = o "\\n"
    } else if (c == "\r") {
      o = o "\\r"
    } else if (c == "\t") {
      o = o "\\t"
    } else if (c == "\b") {
      o = o "\\b"
    } else if (c == "\f") {
      o = o "\\f"
    } else if (b < 32) {
      o = o sprintf("\\u%04x", b)
    } else {
      o = o c
    }
  }
  return o "\""
}

function ord(c,    j) {
  if (ORDINIT == 0) {
    for (j = 0; j < 256; j++) {
      ORD[sprintf("%c", j)] = j
    }
    ORDINIT = 1
  }
  return ORD[c]
}

function fail(reason) {
  print "ERR\t" reason
  exit 1
}

function peek() {
  return substr(src, i, 1)
}

function skip_ws(    c, n2) {
  while (i <= n) {
    c = substr(src, i, 1)
    if (c == " " || c == "\t" || c == "\n" || c == "\r") {
      i++
      continue
    }
    break
  }
  if (i > n) {
    return
  }
  c = substr(src, i, 1)
  if (c == "#") {
    fail("comment")
  }
  if (c == "/") {
    n2 = substr(src, i + 1, 1)
    if (n2 == "/" || n2 == "*") {
      fail("comment")
    }
  }
}

function hexval(h,    j, c, v, d) {
  d = 0
  for (j = 1; j <= length(h); j++) {
    c = substr(tolower(h), j, 1)
    v = index("0123456789abcdef", c) - 1
    if (v < 0) {
      return -1
    }
    d = d * 16 + v
  }
  return d
}

function utf8chr(code,    b1, b2, b3) {
  if (code < 128) {
    return sprintf("%c", code)
  }
  if (code < 2048) {
    return sprintf("%c%c", 192 + int(code / 64), 128 + (code % 64))
  }
  b1 = 224 + int(code / 4096)
  b2 = 128 + (int(code / 64) % 64)
  b3 = 128 + (code % 64)
  return sprintf("%c%c%c", b1, b2, b3)
}

function parse_string_raw(    c, e, hex, code, out) {
  if (peek() != "\"") {
    fail("invalid string")
  }
  i++
  out = ""
  while (i <= n) {
    c = substr(src, i, 1)
    if (c == "\"") {
      i++
      decoded = out
      return 0
    }
    # JSON requires U+0000 through U+001F to be escaped inside a string. Accepting a raw one
    # would make this reader answer where every other JSON reader refuses, and a host with a
    # parser and a host without would then disagree about whether a file is valid at all.
    if (c < " ") {
      fail("invalid string")
    }
    if (c == "\\") {
      i++
      if (i > n) {
        fail("invalid string")
      }
      e = substr(src, i, 1)
      if (e == "\"" || e == "\\" || e == "/") {
        out = out e
      } else if (e == "b") {
        out = out "\b"
      } else if (e == "f") {
        out = out "\f"
      } else if (e == "n") {
        out = out "\n"
      } else if (e == "r") {
        out = out "\r"
      } else if (e == "t") {
        out = out "\t"
      } else if (e == "u") {
        hex = substr(src, i + 1, 4)
        if (length(hex) < 4) {
          fail("invalid string")
        }
        code = hexval(hex)
        if (code < 0) {
          fail("invalid string")
        }
        out = out utf8chr(code)
        i += 4
      } else {
        fail("invalid string")
      }
      i++
      continue
    }
    out = out c
    i++
  }
  fail("invalid string")
}

function jenc(s,    j, c, o) {
  o = "\""
  for (j = 1; j <= length(s); j++) {
    c = substr(s, j, 1)
    if (c == "\\") {
      o = o "\\\\"
    } else if (c == "\"") {
      o = o "\\\""
    } else if (c == "\n") {
      o = o "\\n"
    } else if (c == "\r") {
      o = o "\\r"
    } else if (c == "\t") {
      o = o "\\t"
    } else if (c == "\b") {
      o = o "\\b"
    } else if (c == "\f") {
      o = o "\\f"
    } else {
      o = o c
    }
  }
  return o "\""
}

function unquote_one(text,    save_src, save_n, save_i) {
  save_src = src
  save_n = n
  save_i = i
  src = text
  n = length(src)
  i = 1
  skip_ws()
  if (parse_string_raw() != 0) {
    src = save_src
    n = save_n
    i = save_i
    return 1
  }
  src = save_src
  n = save_n
  i = save_i
  return 0
}

function emit(p1, p2, p3, typ, val) {
  outbuf = outbuf p1 "\t" p2 "\t" p3 "\t" typ "\t" val "\n"
}

function parse_number(    c, start, token) {
  start = i
  c = peek()
  if (c == "-") {
    fail("unknown type")
  }
  if (c == "0") {
    i++
    if (i <= n) {
      c = peek()
      if (c >= "0" && c <= "9") {
        fail("unknown type")
      }
      if (c == "." || c == "e" || c == "E") {
        fail("unknown type")
      }
    }
    return "0"
  }
  if (c < "1" || c > "9") {
    fail("unknown type")
  }
  i++
  while (i <= n) {
    c = peek()
    if (c < "0" || c > "9") {
      break
    }
    i++
  }
  if (i <= n) {
    c = peek()
    if (c == "." || c == "e" || c == "E") {
      fail("unknown type")
    }
  }
  token = substr(src, start, i - start)
  return token
}

function parse_bool(    c) {
  c = peek()
  if (c == "t") {
    if (substr(src, i, 4) != "true") {
      fail("unknown type")
    }
    i += 4
    return "true"
  }
  if (c == "f") {
    if (substr(src, i, 5) != "false") {
      fail("unknown type")
    }
    i += 5
    return "false"
  }
  fail("unknown type")
}

function parse_string_array(p1,    c, idx, first) {
  i++
  skip_ws()
  if (peek() == "]") {
    i++
    emit(p1, "", "", "a", "")
    return
  }
  emit(p1, "", "", "a", "")
  idx = 0
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "]") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("unknown type")
    }
    parse_string_raw()
    emit(p1, idx, "", "s", jenc(decoded))
    idx++
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      first = 0
      skip_ws()
      if (peek() == "]") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "]") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_string_object(    c, key, first) {
  i++
  skip_ws()
  if (peek() == "}") {
    i++
    return
  }
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "}") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("not a JSON object")
    }
    parse_string_raw()
    key = decoded
    if (index(key, "\t") || index(key, "\n")) {
      fail("unexpected nesting")
    }
    skip_ws()
    if (peek() != ":") {
      fail("truncated JSON")
    }
    i++
    skip_ws()
    if (peek() != "\"") {
      fail("unknown type")
    }
    parse_string_raw()
    emit(key, "", "", "s", jenc(decoded))
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "}") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "}") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_rules_object(    c, key, first) {
  i++
  skip_ws()
  if (peek() == "}") {
    i++
    return
  }
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "}") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("not a JSON object")
    }
    parse_string_raw()
    key = decoded
    if (index(key, "\t") || index(key, "\n")) {
      fail("unexpected nesting")
    }
    skip_ws()
    if (peek() != ":") {
      fail("truncated JSON")
    }
    i++
    skip_ws()
    parse_top_value(key)
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "}") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "}") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_top_value(key,    c) {
  c = peek()
  if (c == "\"") {
    parse_string_raw()
    emit(key, "", "", "s", jenc(decoded))
    return
  }
  if (c == "t" || c == "f") {
    emit(key, "", "", "b", parse_bool())
    return
  }
  if (c >= "0" && c <= "9") {
    emit(key, "", "", "n", parse_number())
    return
  }
  if (c == "[") {
    parse_string_array(key)
    return
  }
  if (c == "{") {
    if (key == "toolDeny") {
      emit(key, "", "", "o", "")
      parse_named_string_object(key)
      return
    }
    if (key == "elevation") {
      emit(key, "", "", "o", "")
      parse_elevation()
      return
    }
    if (key == "retention") {
      emit(key, "", "", "o", "")
      parse_retention()
      return
    }
    if (key == "shift" || key == "handoff" || key == "archive" || key == "recovery" ||
        key == "receipts") {
      emit(key, "", "", "o", "")
      parse_settings_object(key)
      return
    }
    fail("unexpected nesting")
  }
  if (c == "n") {
    fail("unknown type")
  }
  if (c == "-") {
    fail("unknown type")
  }
  fail("unknown type")
}

function parse_named_string_object(p1,    c, key, first) {
  i++
  skip_ws()
  if (peek() == "}") {
    i++
    return
  }
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "}") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("not a JSON object")
    }
    parse_string_raw()
    key = decoded
    if (index(key, "\t") || index(key, "\n")) {
      fail("unexpected nesting")
    }
    skip_ws()
    if (peek() != ":") {
      fail("truncated JSON")
    }
    i++
    skip_ws()
    if (peek() != "\"") {
      fail("unexpected nesting")
    }
    parse_string_raw()
    emit(p1, key, "", "s", jenc(decoded))
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "}") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "}") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_elevation(    c, cat, first) {
  i++
  skip_ws()
  if (peek() == "}") {
    i++
    return
  }
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "}") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("not a JSON object")
    }
    parse_string_raw()
    cat = decoded
    if (index(cat, "\t") || index(cat, "\n")) {
      fail("unexpected nesting")
    }
    skip_ws()
    if (peek() != ":") {
      fail("truncated JSON")
    }
    i++
    skip_ws()
    if (peek() != "{") {
      fail("unexpected nesting")
    }
    emit("elevation", cat, "", "o", "")
    parse_named_string_object_pair("elevation", cat)
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "}") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "}") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_named_string_object_pair(p1, p2,    c, key, first) {
  i++
  skip_ws()
  if (peek() == "}") {
    i++
    return
  }
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "}") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("not a JSON object")
    }
    parse_string_raw()
    key = decoded
    if (index(key, "\t") || index(key, "\n")) {
      fail("unexpected nesting")
    }
    skip_ws()
    if (peek() != ":") {
      fail("truncated JSON")
    }
    i++
    skip_ws()
    if (peek() != "\"") {
      fail("unexpected nesting")
    }
    parse_string_raw()
    emit(p1, p2, key, "s", jenc(decoded))
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "}") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "}") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_null() {
  if (substr(src, i, 4) != "null") {
    fail("unknown type")
  }
  i += 4
}

# A settings block: one level of named values, each a string, integer, bool, null,
# or the string array the schema declares. Deeper nesting still fails closed - this
# reads the shape the schema describes, never arbitrary JSON.
function parse_settings_object(p1,    c, key, first) {
  i++
  skip_ws()
  if (peek() == "}") {
    i++
    return
  }
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "}") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("not a JSON object")
    }
    parse_string_raw()
    key = decoded
    if (index(key, "\t") || index(key, "\n")) {
      fail("unexpected nesting")
    }
    skip_ws()
    if (peek() != ":") {
      fail("truncated JSON")
    }
    i++
    skip_ws()
    c = peek()
    if (c == "\"") {
      parse_string_raw()
      emit(p1, key, "", "s", jenc(decoded))
    } else if (c == "t" || c == "f") {
      emit(p1, key, "", "b", parse_bool())
    } else if (c >= "0" && c <= "9") {
      emit(p1, key, "", "n", parse_number())
    } else if (c == "n") {
      parse_null()
      emit(p1, key, "", "z", "null")
    } else if (c == "[") {
      parse_settings_array(p1, key)
    } else {
      fail("unexpected nesting")
    }
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "}") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "}") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_settings_array(p1, p2,    c, idx, first) {
  i++
  skip_ws()
  emit(p1, p2, "", "a", "")
  if (peek() == "]") {
    i++
    return
  }
  idx = 0
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "]") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("unknown type")
    }
    parse_string_raw()
    emit(p1, p2, idx, "s", jenc(decoded))
    idx++
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "]") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "]") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

function parse_retention(    c, key, first) {
  i++
  skip_ws()
  if (peek() == "}") {
    i++
    return
  }
  first = 1
  while (1) {
    skip_ws()
    c = peek()
    if (c == "}") {
      if (first == 0) {
        fail("trailing comma")
      }
      i++
      return
    }
    if (c != "\"") {
      fail("not a JSON object")
    }
    parse_string_raw()
    key = decoded
    if (index(key, "\t") || index(key, "\n")) {
      fail("unexpected nesting")
    }
    skip_ws()
    if (peek() != ":") {
      fail("truncated JSON")
    }
    i++
    skip_ws()
    c = peek()
    if (c < "0" || c > "9") {
      fail("unexpected nesting")
    }
    emit("retention", key, "", "n", parse_number())
    first = 0
    skip_ws()
    c = peek()
    if (c == ",") {
      i++
      skip_ws()
      if (peek() == "}") {
        fail("trailing comma")
      }
      continue
    }
    if (c == "}") {
      i++
      return
    }
    fail("truncated JSON")
  }
}

SHA-256: af1e36318275e121b2c965935c5b8a9e0a007bf9fd646a3fbf3b6b280eec672a