AUD-49: build the Hub skeleton (hear-route-answer)

- Three inbound routes from docs/hub-api.md: POST /ask, POST /approve, GET /health.
- Five-step pipeline in src/hub, with packages hear, understand, check, route, answer.
- Routing through OmniRoute with the capability list and routing.md. The
  never-route control check runs before any LLM call.
- One real destination: Jervis house TTS, started out of band.
- Fake /ask project (src/tests/fakeproject) for the round-trip test.
- Append-only, size-capped request log. Bearer auth, source match, rate limit.
- Tests: all packages pass, including the fake /ask round trip. Race clean.
- Dockerfile and compose updated to mount routing.md and secrets, log on tmpfs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-10 17:38:24 +11:00
parent 81f9722eee
commit bf80efa25d
33 changed files with 4025 additions and 110 deletions

87
src/ids/ids.go Normal file
View File

@@ -0,0 +1,87 @@
// Package ids makes the request and approval identifiers from docs/hub-api.md.
// The shape is a ULID: 26 Crockford base32 characters, time-ordered. It needs
// no dependency, so the Hub stays a small static binary.
package ids
import (
"crypto/rand"
"sync"
"time"
)
// Crockford base32 alphabet. ULID uses these 32 symbols.
const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
var (
mu sync.Mutex
lastMS uint64
lastRnd [10]byte
)
// New returns a 26-character ULID string. The first 10 characters hold the
// time. The last 16 hold randomness. Two calls in the same millisecond do not
// repeat.
func New() string {
mu.Lock()
defer mu.Unlock()
ms := uint64(time.Now().UnixMilli())
if ms == lastMS {
// Same millisecond: increment the random part.
for i := len(lastRnd) - 1; i >= 0; i-- {
lastRnd[i]++
if lastRnd[i] != 0 {
break
}
}
} else {
lastMS = ms
if _, err := rand.Read(lastRnd[:]); err != nil {
// crypto/rand does not fail in practice. Fall back to the clock.
for i := range lastRnd {
lastRnd[i] = byte(ms >> (8 * (i % 8)))
}
}
}
return encodeTime(ms) + encodeRandom(lastRnd)
}
// Approval returns an approval id, for example "ap_01J8Z...".
func Approval() string {
return "ap_" + New()
}
// encodeTime writes 10 base32 characters from the low 50 bits of ms.
func encodeTime(ms uint64) string {
var out [10]byte
for i := 9; i >= 0; i-- {
out[i] = alphabet[ms&0x1f]
ms >>= 5
}
return string(out[:])
}
// encodeRandom writes 16 base32 characters from 10 random bytes (80 bits).
func encodeRandom(rnd [10]byte) string {
var out [16]byte
var hi, lo uint64
hi = uint64(rnd[0])<<8 | uint64(rnd[1])
hi = hi<<8 | uint64(rnd[2])
hi = hi<<8 | uint64(rnd[3])
hi = hi<<8 | uint64(rnd[4])
lo = uint64(rnd[5])<<8 | uint64(rnd[6])
lo = lo<<8 | uint64(rnd[7])
lo = lo<<8 | uint64(rnd[8])
lo = lo<<8 | uint64(rnd[9])
for i := 15; i >= 8; i-- {
out[i] = alphabet[lo&0x1f]
lo >>= 5
}
for i := 7; i >= 0; i-- {
out[i] = alphabet[hi&0x1f]
hi >>= 5
}
return string(out[:])
}