The controller (oldbox) sent a data-model alignment proposal and asked for six
additive changes on this node, the validation protocol, and an honest report.
Six of the seven changes are done; the seventh is declared missing rather than faked.
Stage 0 - reachability verified exactly as documented: health 200, heartbeat 401
(the correct answer - auth is refusing), missing Host 400. Nothing else was sent.
source_unit_key (controller 5.1, the blocking item)
umbraaudio/source_units.py. sha256("uk1" | chapter_source_text_sha256 |
occurrence | text_sha256). Nothing positional in the key: "occurrence"
disambiguates repeated text in document order.
- 18,181/18,181 units keyed across all four projects, 0 collisions
- chapter 1 renumbered +100 -> 0 keys changed (the load-bearing proof)
- backfill 10,015 rows in 0.11s, second run stamps 0 (idempotent)
- alignment phase 3: 200/200 probe units resolved with used_index: false
- tolerates the older projects that have no chapters.chapter_index
Recorded at generation time in chapter_builder, BEFORE the rows are read and
BEFORE the marker is written, so every path that renders audio also produces
identity and the clip can name its source unit.
5.7 recorded: utterance_index is KEPT as a display ordinal and made
non-load-bearing. spec 21 §10.
Blob hashes and effect chain (5.2)
Markers now carry the full sha256 of the delivered bytes and the effects that
produced them. Verified the existing order is already correct - write_marker
runs AFTER apply_effects - and closed the remaining hole: a changed trait list
changed the hash with no recorded cause. sha256 verification is opt-in, since
hashing every file on every validation pass would re-read gigabytes.
Vocabularies published (5.4) - umbraaudio/definitions.py, 19 sets, 126 values,
each value carrying whether it is 'declared' (spec/controller) or 'extracted'
(read out of code), so a diff shows a difference of meaning, not of wording.
Alignment report tool (6.6) - scripts/alignment_report.py --phase all. Emits the
controller's envelope with the per-phase evidence shapes, and obeys the two
rules they set: no evidence means blocked, never pass; a field we cannot produce
is null PLUS a finding, never omitted. Phase 2 is deliberately 'fail' - the
three-way store disagreement is real, and a pass there would be a check that
passed by doing nothing.
Fingerprint discipline (5.5) - the drift detector reported its OWN fingerprint
change when source_unit_key was added rather than hiding it. Node profile
updated with the new fingerprints and an honest note that they moved.
Node report extended - per-project source-unit-key coverage, per-voice-asset
sha256 + object_key, definition_set_version, controller stage reached.
Six gaps found in the proposal (spec 21 §11), including the one they invited plus
five more. The sharpest: "hash at generation" ignores that audio is
post-processed afterwards, and our key is anchored to chapters.source_text - a
second copy of the manuscript - so any change to chapter storage silently
invalidates every key in that chapter.
5.6 deliberately NOT faked: personality has no versioned artifact here, only
unversioned LLM prose in speakers.json. Reported as phase 6b finding B-03
instead, because wrapping unversioned inputs in a version field would produce
artifacts claiming a lineage they do not have.
Answered the controller's two open questions (infra/controller-handoff-2026-10-03.md):
forward-only link for a revealed recurring character, and yes - a new edition may
contradict a frozen state, since this node's correction path IS a regeneration.
Full suite 484 passed / 8 failed - the same 8 pre-existing and unrelated failures.
ruff clean on every file touched. The parallel session's chapter_builder hunk
(helper_url) was filtered out of the commit and left intact in the working tree.
41 KiB
Spec 21 — The Umbra System: Worker Nodes, Task Assignment, Cross-Product Change Management, and UI Topology
- Status: PROPOSAL. Nothing here is ratified. Every table marked proposed is a draft contract for the controller to accept, amend, or reject.
- Date: 2026-10-01
- Audience: every machine running an Umbra product, plus the control plane.
- Owner of ratification: the main Umbra system (controller, currently
oldbox,192.168.123.14). This document was written by a worker and proposes; it does not decide. - Companion documents (do not duplicate them, link to them):
manifest.md— authoritative inventory of what UmbraAudio actually holds (898 lines).specs/20-enterprise-database-integration.md— shared-database contract, blockers, decisions.specs/19-enterprise-control-plane.md— tenancy, roles, jobs, lineage.specs/18-UmbraTrans.md— translation/multilingual production.infra/README-enterprise-bootstrap.md— the control-plane database bootstrap that already exists onoldbox.AGENTS.md— this machine's operational rules and hazards.
0. How to use this document
This machine (a worker) has written it so that the other machines and the controller do not have to re-derive the same facts. Three concrete things every other machine should do:
- Read §5 and §6 of
manifest.md— that is where the real data inventory is. Do not re-invent it. - Produce a node report with
scripts/umbra_node_report.py(this machine's implementation, §8.2). It is read-only, prints no secrets, and emits a schema fingerprint per project. Copy the script; it has no product-specific imports beyondumbraaudio.config. - Declare your node in your own
infra/umbra-node.jsonin the shape of §4.4, then runscripts/umbra_node_report.py --checkand get exit 0 before claiming you are aligned.
1. Executive summary
| # | Workstream | Where it is | What is missing |
|---|---|---|---|
| WS1 | Align the data model, copy UmbraAudio data in, verify, later transition | Design is unusually far along: manifest.md + spec 20 |
Ratification. Nine open decisions, five hard blockers, zero implementation. |
| WS2 | Every machine is a node that gets assigned tasks (not only audio) | Job types, leases, heartbeats and pools are specified (spec 19 §12) | Node contract: no node identity, no capability advertisement, no task envelope, no reporting rule. Proposed in §4. |
| WS3 | How changes are managed across products | Nothing exists. Specs 19/20 cover data changes, not code/requirement changes | Requirement catalog + distribution + confirmation loop. Proposed in §5. |
| WS4 | Does a central server take over the UI? How do updates get deployed and tested? | DECISION-8 is open and unanswered |
A decision and a deployment/update flow. Recommendation + options in §6. |
The single most important cross-cutting fact: this machine already contains a complete, measured data inventory and a verified shared-database contract. The system does not need another discovery phase. It needs someone to ratify decisions, then implement. Every week spent re-discovering is a week not spent building.
2. System shape (proposed, matches spec 19 §4)
┌──────────────────────────────────────┐
│ CONTROLLER (oldbox 192.168.123.14) │
│ • authoritative data (PostgreSQL) │
│ • identity, tenancy, authorization │
│ • requirements catalog (§5) │
│ • definition sets (§5.7) │
│ • task queue + leases (§4) │
│ • lineage, artifacts, releases │
│ • portal UI (status only, §6) │
└───────┬──────────────┬───────────────┘
│ pull tasks │ report results
┌─────────────────┴───┐ ┌───┴──────────────┐
▼ ▼ ▼ ▼
┌───────────────┐ ┌──────────────┐ ┌────────────────────┐
│ node: audio │ │ node: trans │ │ node: <future> │
│ mini.bdmurphy │ │ <other host> │ │ │
│ │ │ │ │ │
│ UmbraAudio │ │ UmbraTrans │ │ other products │
│ web UI :8420 │ │ │ │ │
│ TTS :8881 │ │ │ │ │
└───────────────┘ └──────────────┘ └────────────────────┘
Authority rule (already proposed in spec 20 §A.3–A.4, needs ratification):
- The controller is authoritative for identity, tenancy, requirements, definitions, task state, lineage and release decisions.
- A node is authoritative for its own working area (SQLite + filesystem) while it is in
legacyormirroringstate, and it must keep working when the controller is unreachable. - Nothing is ever a cutover for the whole system at once. Migration state is per project
(
legacy → mirroring → dual_write → authoritative → retired; manifest §14.1).
3. WS1 — Align the data model, copy the data, verify, then transition
3.1 What already exists (do not redo)
manifest.md— every data object, exact DDL, JSON shapes, artifact naming, the legacy→shared entity mapping (§13), 14 open questions (§15), and the aligned-definition-set list (§14.2).specs/20— table-by-table contract, cutover phases P1–P4, RLS review, acceptance tests.infra/postgres/001_control_plane.sql— 13 real tables, already bootstrapped onoldbox.- Measured state of this machine, reproducible today:
scripts/umbra_node_report.py.
3.2 The five headline facts any aligning machine must know
- The SQLite is mostly empty. Only
project,chapters,utterances,name_tokenshold data.characters,generated_audio,pronunciation_guide,quality_reports,review_decisions,exception_reviews,voice_review_historyare empty in every project. A SQL-only migration loses the speaker model, the voice model, the pronunciation model and the entire audio record. - There is no durable utterance identity. Files are
utt_{utterance_index:04d}.wavandutterance_indexis renumbered by re-segmentation and by utterance deletion. Spec 18 §3.3 forbids mutable indices as cross-language IDs. Spec 20 §E.1 proposes a content-addressedsource_unit_keyrecorded at generation time with filenames unchanged — this machine can build that today with no dependency on the controller. - Speaker identity is free text.
utterances.speaker_nameis the only link to a speaker; it is not a foreign key, not normalised, and has case variants. Five parallel stores, four key schemes, three-way count disagreements (Last Dwarf93/68/71 andDealHunter61/64/97 as of 2026-10-01). - The control-plane worker role is a blocker.
umbracontrol_workermatches zero RLS rows and cannot attribute an audit event (spec 20B1). Needsservice_identities+job_leases, not a GUC workaround.B4(functionsEXECUTE-able byPUBLICwith noALTER DEFAULT PRIVILEGES) must be fixed before any further migration. - Per-project schema drift is real and measurable. This machine currently has two schema
fingerprints:
0078569a0fa6bc70(Last Dwarf, DealHunter) and367d5df9e546dbba(Treasure Island, Vocals — missingname_tokensand nine utterance columns). A migration must introspect per database, never assume a uniform shape.
3.3 Sequence (proposed)
| Phase | Name | Gate to pass | Who |
|---|---|---|---|
| P0 | Ratify the nine decisions in §6.1 | Decisions recorded in the controller | Controller |
| P1 | Adapter/repository boundary inside UmbraAudio; no behaviour change | Existing tests unchanged; "where state lives" becomes configuration | Worker |
| P2 | Read-only mirror of the audio data into the controller | §3.4 verification passes on all 4 projects | Worker + controller |
| P3 | Dual-write; controller authoritative, worker still functional offline | No divergence beyond an agreed window; leases heartbeat | Both |
| P4 | Per-project authoritative; local SQLite becomes read-only cache |
Lineage reproducible; released lineage immutable | Controller |
3.4 Verification gates for "it all works" (proposed)
- Every project imports with zero dropped rows; ambiguous/unresolved values are recorded, not
discarded (
legacy_id_maprows with a reason). - Row counts match the node report for every tracked table.
- Schema fingerprints are recorded per source database, and drift is reported, not normalised silently.
- Every utterance resolves to a
speaker_idor an explicitneeds_source_reviewrow. - Every voice has a SHA-256 and an
object_key; no binary in PostgreSQL. - A sample chapter can be re-imported and produces an identical hash set (idempotency).
- Proven: importing does not create or modify a single file in the source project tree (before/after tree hash).
- The audio worker completes a real chapter with the controller deliberately stopped.
4. WS2 — Every machine is a node that receives tasks
4.1 Why this needs its own contract
Spec 19 §12 already defines job types, leases, heartbeats, pools and idempotency keys. What is missing is the node half: how a machine announces what it is, what it can do, what it refuses, and how it reports a finished task. Without that, "assign a task to a machine" cannot be expressed, let alone executed.
4.2 Node identity (proposed)
node_id: stable, human-readable, lowercase-hyphenated, never reused. This machine:umbraaudio-m4.role:controller | worker.products: which products the node runs (["umbraaudio"]).node_contract_version: the version of this contract the node implements. Bump on breaking change. A node that reports an unknown version is not eligible for tasks.- Identity is declared in the repository, not configured by hand in a UI (§4.4).
4.3 Capability advertisement (proposed)
A node advertises capabilities (what it can do), resource_pools (what it has, with
concurrency), and constraints (what it must not do). The scheduler matches a task's required
capability list against this advertisement — it does not hard-code hostnames.
Constraint declaration matters as much as capability declaration. This machine, for example, must refuse any task requiring an LLM and local TTS simultaneously, because 16 GB of unified memory cannot hold both and doing so silently degrades output to Edge-TTS.
4.4 Node profile file (proposed, implemented here)
Each node keeps infra/umbra-node.json — identity, services, capabilities, pools, constraints,
project list, schema fingerprints, and the honest gaps. This machine's file is
infra/umbra-node.json; it contains no secrets (ids, names, labels, counts only).
4.5 Task envelope (proposed)
task:
task_id: tsk_01J... # opaque, controller-issued, globally unique
requirement_id: REQ-... # §5 — every task traces to a requirement
product: umbraaudio
capability: analysis.speaker_attribution
target_node: umbraaudio-m4 # null = scheduler chooses by capability
scope: # what the task operates on
project: "Last Dwarf"
locale: null
revision: <source_revision_id or null>
instruction: "Re-run attribution for ch11-49 after the designation merge."
inputs: # immutable references, never inline blobs
definition_set_version: defs-2026-10-01
source_snapshot_id: snap_...
acceptance_criteria: # machine-checkable where possible
- "pytest tests/ -q exits 0"
- "node report --check exits 0"
- "utterance.chapter_id in (11..49) all have attribution_confidence NOT NULL"
idempotency_key: sha256(...) # re-delivery must not duplicate work
lease_ttl_s: 900
deadline: 2026-10-08T00:00:00Z
report_back: # required in the result, not optional
- changed_row_count
- before_after_fingerprint
- test_evidence
- definition_set_version_used
- notes
4.6 Result envelope (proposed)
result:
task_id: tsk_01J...
status: succeeded | failed | refused | partial
refusal_reason: "..." # required when refused
evidence: { ... } # acceptance_criteria satisfied, with proof
node: { node_id, node_contract_version, git_head, dirty }
facts: { ... } # the read-only node report, or the relevant subset
A node may refuse a task and must say why; refusing is a valid, audited outcome. Silently producing wrong output is not.
4.7 What this machine can accept today
audio.parse_manuscript, audio.segment_utterances,
analysis.{character_discovery,speaker_attribution,speaker_clarity,name_token_scan},
voice.{reference_generation,attribute_measurement},
audio.{render_utterances,stitch_chapters,normalize_loudness}, export.{m4b,mp3_package},
review.speaker_QA.
It cannot accept: translation, TTS on CUDA, anything needing an authenticated UI, anything requiring concurrent LLM + local TTS.
4.8 Transport (proposed; a parallel session is already building one)
The envelopes in §4.5–4.6 need a transport between a node and the controller. A parallel
session in this repository is already building one: an agent-comm stdio MCP server with an
append-only inbox/outbox mailbox on a NAS share (infra/agent_bridge/,
infra/oldbox-agent-communication-prompt.md, in flight and uncommitted as of 2026-10-01).
If that lands, the recommendation is:
- Reuse it for node↔controller messaging rather than inventing a second channel.
- The task envelope (§4.5) becomes message
kind: instruction; the result envelope (§4.6) becomeskind: report, correlated bytask_id. - Requirement records (§5.2) become
kind: requirement+kind: ack. - The node report (§8.2) is the
evidencepayload, sanitized — it already contains no secrets. - Its security rules become system rules: sanitized text/JSON only, atomic writes, no secrets in any message, no unauthenticated network listener.
One correction this document adds to that in-flight work: a mailbox is a transport, not a scheduler. It carries tasks; it does not decide who may run what. That remains the controller's job, and leases/heartbeats must not be implemented as "a file in a folder".
5. WS3 — How changes are managed across products
This is the least-specified part of the system today and the one the operator cares most about. Proposal below.
5.1 The model
The controller owns requirements and definitions. Products own implementation. A change is never pushed into a product. It is distributed as a requirement, implemented locally, and then confirmed with evidence. The requirement cannot close until every affected product confirms or explicitly waives.
This preserves two real facts: (a) some products make changes locally and commit in git; (b) some changes must apply everywhere. Neither needs the other to be overruled.
5.2 Requirement record (proposed)
requirement:
requirement_id: REQ-0042
title: "Unresolved speaker designations must exist as speakers"
rationale: "A designation with no profile resolves to a random named character."
change_class: cross_product # local_product | cross_product | shared_definition | operational
owned_by: controller
definition_set_version: defs-2026-10-01 # null for local_product
scope:
products: [umbraaudio, umbratrans]
nodes: [umbraaudio-m4] # empty = all nodes running those products
acceptance_criteria: ["each product reports its own conformance measurement"]
state: proposed | triaged | distributed | in_progress | confirmed | waived | released
confirmations: # one per product/node
- product: umbraaudio
node: umbraaudio-m4
state: confirmed
evidence: {name_tokens_resolved: 10, unresolved_designations: 0, report_at: "..."}
commit: "4775e3b"
5.3 The four change classes (proposed)
| Class | What changes | Distribution | Confirmation evidence |
|---|---|---|---|
local_product |
One product's internals (UI, performance, refactor) | Requirement records intent and outcome; no code is sent | Product reports resulting version/commit |
cross_product |
Same behaviour needed in N products | Requirement per product, each with its own criteria | Each product's own measurement |
shared_definition |
Vocabulary/schema/attribute definitions | Versioned data, not code. Products pull the set and record which version they used | definition_set_version_used + applied row counts |
operational |
Env, config, deploy, credentials-ish | Per-node task; the node reports state | Node report facts (e.g. ports, fingerprints) |
5.4 The worked example the operator asked about
"A user added or changed something that must apply across the system, owned and managed by the main controller."
The real cases in this system are a speaker designation, a name pronunciation, a voice attribute, or a role kind. Proposed handling:
- The controller holds the entity as master data with an identity, evidence, and a revision.
- It emits a
shared_definitionrequirement: "add termXwith spellingY, provenanceexplicit, effective for products P1 and P2, definition setdefs-2026-10-02". - Each product applies it in its own representation — UmbraAudio writes
phonetic_names.json+speakers.json; UmbraTrans writes its own pronunciation table. The controller never writes another product's files. - Each product confirms with a measurement: "term present, spelling matches, N utterances affected" — not "done".
- Until every product confirms, the requirement stays
in_progressand is visible as such. - A local product-side discovery of the same fact (e.g. the designation scanner found 23 missing speakers here) is reported upward as evidence, and the controller decides whether it becomes master data. This is how the worker's discovery becomes system knowledge.
5.5 Code updates: distribution and testing (proposed)
Code ships through git, per product repository. The controller's job is not to carry code; it is to carry the compatibility matrix and the requirement.
controller: REQ "move umbraaudio to tag v0.9 + definition set defs-2026-10-02"
↓ distributed (one task per node)
node: git fetch && git checkout v0.9
run the product's own verification:
- pytest tests/ -q (this machine: 461 passing)
- scripts/umbra_node_report.py --check (profile/contract still valid)
- a product smoke test (start web, hit one endpoint)
record: commit SHA, test counts, report fingerprint, definition_set_version_used
↓ confirm
controller: closes REQ only when every target node confirms or explicitly waives
Rules:
- A node that cannot reach the controller keeps working and queues its confirmation.
- A node never auto-updates itself without a task. Audio production must not stop because someone merged.
- Definition sets are pulled by version and recorded; a node using an old set is visible, not silently wrong.
- Every product repo keeps its own
AGENTS.mdhazards. The system-level hazard list (§8.6) is additive, never a replacement.
5.6 What must NOT be centrally managed
Product-internal implementation, local caches, generated audio, model weights, TTS prompt caches, per-node environment tuning, and anything machine-specific (this machine's external volume, TCC rules, its own launchd layout).
6. WS4 — Where does the UI live?
6.1 The decision (proposed, answers the operator's question)
Neither extreme. Hybrid, with the controller authoritative for data and the worker keeping the program UI.
The controller does take on the UI function, but only the system UI: a portal showing node health, task state, requirement conformance, definition-set versions, project/work registry and release status. It does not host the audio review UI.
Why:
- The audio review UI is bound to local artifacts — WAV players, per-utterance regeneration, staleness markers, local voice references. A central UI would have to stream every artifact across the network to reproduce it.
- That UI has no authentication at all (spec 20 §J.6). Centralising it would publish an unauthenticated mutation surface to the internet. It must stay on the LAN regardless.
- Its real risk is staleness and fragility, not location. A node can be perfectly robust in place and useless if it cannot report what version it is and whether its data still agrees with the controller.
6.2 Options considered
| Option | Pros | Cons |
|---|---|---|
| Central UI only | One place, one auth, one version | Streams all artifacts; couples UI downtime to production; biggest rewrite; publishes attack surface today |
| Worker UI only (today) | Fast, local, artifact-native | No cross-project view; a dead node is invisible; updates drift silently |
| Hybrid (recommended) | Portal on the controller; program UI stays on the worker; signed deep links; version + conformance shown on both | Two UIs to keep coherent |
6.3 How the two UIs stay coherent (proposed)
- The worker UI displays, in its footer,
node_id,definition_set_version, and the last successful confirmation timestamp. A reviewer can always see which system's view they are in. - The portal deep-links into the worker UI with a short-lived signed link. No credentials travel in a URL.
- Any authoritative decision (approve, merge speaker, release, publish) is written in the controller and read by the worker UI; the worker UI never becomes the source of truth for one of those.
6.4 How worker UI robustness is assured (proposed)
- Node report as the health probe.
scripts/umbra_node_report.py --checkis the definition of "this node is healthy and matches its declared profile". - Existing process discipline. This machine already runs the UI under a launchd
KeepAliveagent with a health-gated startup. That pattern is the baseline for every node. - Read-only by default. Sync and pull operations never mutate project data without an explicit task.
- Degradation is normal. If the controller is down, the worker keeps producing audio from its local lease and reconciles later. Production availability outranks synchronization.
- No silent exposure. While the UI is unauthenticated, it must remain LAN-only. The reverse proxy must not publish mutation endpoints. This is a precondition, not a nice-to-have.
6.5 Decisions this workstream needs (proposed answer)
- Ratify the hybrid (portal on controller, program UI on worker).
- Decide whether the audio UI is retired later or kept permanently. Recommendation: keep. It is the program UI; it is what a director uses while a book is in production.
- Make the open security defects (§8.5) a precondition for any change in exposure.
7. Questions that need answers
Grouped by who can answer. Each says what it blocks.
7.1 System topology and authority
- Is the controller's database authoritative for the audio system? (spec 20
DECISION-1; this document §2 assumes yes). Blocks: everything in WS1. - Does UmbraTrans read the authoritative database instead of snapshotting? (
DECISION-2). If yes, spec 18 §4.2.1 is superseded and a hard class of bugs disappears. Blocks: the adapter. - One node per product, or one node running several products? This machine currently runs only UmbraAudio but already hosts the GPU box's relay. Affects leases and blast radius.
- What is the official home of the requirement catalog? The control-plane PostgreSQL is the proposal; it must be decided before any requirement tooling is built.
7.2 Data model
- Who resolves an unresolvable speaker name — create a placeholder, or record it unresolved? (manifest §15.1). Blocks: the translation identity gate, which is fail-closed on exactly this.
- Which of the three speaker stores wins when they disagree (
Last Dwarf93/68/71,DealHunter61/64/97 — manifest §15.2)? - Is
utterance_indexpreserved as a display ordinal or retired in favour of a content-addressed key (manifest §15.4)? Recommendation: keep it as a display ordinal, addsource_unit_key. - Is the manuscript stored once or duplicated in
chapters.source_textandutterances.text(manifest §15.5)? Affects copyright handling. - Which voice-attribute profile is canonical — the 9 measured features, the prose taxonomy, or the 13-dimension acoustic set (manifest §15.14)? Recommendation: store measurements and definitions separately.
- Does one voice need a separate vmatrix per target language? (manifest §15.11).
Recommendation: per
(voice, locale), with a marked locale-neutral option.
7.3 Tasks and nodes
- What is the node identity scheme, and who issues it? (§4.2).
- Does the scheduler match on declared capability only, or may a task name a node? Recommendation: capability-first, node as a hint.
- What happens when a node refuses a task? Recommendation: valid, audited, visible, and the requirement stays open.
- Are tasks ever allowed to require LLM + TTS co-residency on a 16 GB machine? Recommendation: forbid it as a capability constraint, not a per-node convention.
7.4 Change management
- Is the requirement catalog part of the control-plane database, or a separate system?
- What is the confirmation standard of proof — a test result, a measurement, a human approval, or all three? (§5.2).
- Who arbitrates when two products need incompatible versions of a shared definition?
- How long may a requirement stay
in_progressbefore it is escalated? There is no clock in this proposal.
7.5 UI and deployment
- Ratify the hybrid UI topology (§6.1).
- Does every product deploy by git tag pull, or is there a package registry?
- Who may push a definition-set version, and does it require review?
- Which host runs object storage, and is it the same host as PostgreSQL? (spec 20
DECISION-6). Until it exists, artifacts stay on local filesystems and the database holds only resolvable keys.
7.6 Security preconditions
- Raterify the
B1remedy (service_identities, not a GUC workaround). - Raterify
B4— revokeEXECUTEonappfunctions and set default privileges before any further migration. - Who signs off on exposing any worker UI beyond the LAN, given zero authentication today?
8. What UmbraAudio already provides for the whole system
8.1 The high-value assets (not just data)
| Asset | Where | Reuse value across machines |
|---|---|---|
| Complete measured data inventory | manifest.md |
Removes the discovery phase from every other machine |
| Verified shared-DB contract + blockers | specs/20 |
Saves an entire architecture exercise |
| Control-plane schema, live | infra/postgres/001_control_plane.sql, bootstrapped on oldbox |
The system already has a database; it lacks domain tables |
| Data-manifest generator | manifest.md + scripts/umbra_node_report.py |
Any machine can produce the same fingerprint report |
| Node profile format | infra/umbra-node.json + scripts/umbra_node_report.py |
Copyable node declaration |
| Legacy→shared entity mapping | manifest.md §13 |
The actual migration mapping table, already written |
| Aligned-definition set list | manifest.md §14.2 |
The exact list of definitions to extract from code into data |
| Verification culture | 447→461 passing tests, ruff, mypy, per-session acceptance criteria | A node can confirm with evidence rather than assertion |
| Operational hazard knowledge | AGENTS.md |
Saves every machine from rediscovering TCC/PATH/co-residency traps |
8.2 Reproducible facts about this machine (today)
.venv/bin/python scripts/umbra_node_report.py # human-readable
.venv/bin/python scripts/umbra_node_report.py --json # machine-readable
.venv/bin/python scripts/umbra_node_report.py --check # confirm profile; exit 2 on drift
- 4 projects, 2 distinct schema fingerprints (real drift).
- Only 4 of 11 tables carry data.
- Speaker identity lives in JSON + WAVs; audio record lives on the filesystem + markers.
- No authentication, no shared-Postgres client, no lease/heartbeat.
- 461 tests passing; 8 known pre-existing/parallel failures.
8.3 Requirements of this data that the shared model must satisfy
From manifest.md §10, the blocking ones are: stable source-unit identity, stable speaker
identity, an immutable source revision, and artifact lineage with SHA-256. The rest are
available today and can migrate as-is.
8.4 What the worker must never do
Write into another product's data area; renumber or rewrite utterances; reuse English audio markers as target-language markers; call another product's private functions; require a migration before the controller is ready.
8.5 Defects that constrain any exposure or migration (from manifest.md §12)
No authentication anywhere (65 mutating endpoints, plain HTTP, 0.0.0.0 bind); project
selection is an unsigned cookie; download traversal via string-prefix containment; upload
filename not sanitised; no job queue/lease/heartbeat; four divergent DDL authorities; non-atomic
JSON writes; no artifact checksums; foreign keys never enabled.
8.6 Hazards on this machine specifically
External-volume TCC (only the venv python may touch the volume under launchd); launchd's bare
PATH; macOS Local Network TCC blocking launchd processes from LAN IPs; Qwen and LM Studio
never co-resident; PROJECT_BASE hard-coded; concurrent editors in this repository.
9. This machine's plan — task list
Legend: NOW = no dependency on the controller, safe today. NEXT = needs a decision or another machine. LATER = needs the shared database to exist. Each task has an acceptance criterion so it can be confirmed rather than reported done.
9.1 NOW
| ID | Task | Why | Acceptance | Est |
|---|---|---|---|---|
M4-01 |
Node profile + read-only node report + --check |
Make this machine describable to the system | infra/umbra-node.json; --check exits 0 |
done 2026-10-01 |
M4-02 |
Commit this session's artifacts and document them | AGENTS.md cadence rule; nothing left uncommitted | Commit exists; session recorded in AGENTS.md |
15 min |
M4-03 |
Reconcile the dirty working tree | 20 files uncommitted from parallel sessions; a lost day of work is a real loss here | Working tree clean, or every uncommitted hunk accounted for | 1 h |
M4-04 |
Add source_unit_key (content-addressed, additive) |
The one blocking identity concept that needs no controller. Without it, the mirror imports unstable IDs | done 2026-10-03 — umbraaudio/source_units.py; 18,181/18,181 units keyed across 4 projects, 0 collisions; renumbering changes 0 keys; 200/200 probe units resolve without the index; marker carries the key |
3-4 h |
M4-05 |
Extract the aligned definition set into data | manifest.md §14.2 — vocabularies currently live in code, so every product reimplements them |
half done 2026-10-03 — umbraaudio/definitions.py + published definitions/umbraaudio-defs.json (19 sets, 126 values, per-value provenance). Remaining: make the code read the sets instead of its own literals |
3-5 h |
M4-06 |
Fix the cheap security defects | Precondition for any exposure change, and for an honest node posture | Traversal + filename fixes with tests | 2 h |
M4-07 |
Resolve the speaker-count disagreement | 70/93/71 — the single most confusing fact about this machine's data | One authoritative count per project, with the others derived or explicitly deprecated | 2-3 h |
M4-08 |
Add a read-only /api/node health endpoint |
Lets the controller scrape node health without an agent; LAN-only | Endpoint returns the node report; no secrets; LAN-only documented | 1-2 h |
M4-09 |
Controller Stage 0 — reachability | Prove the node is reachable and that auth actually refuses | done 2026-10-03 — 200 / 401 / 400, exactly as the controller documented | 15 min |
M4-10 |
Controller Stage 1 — send the node report as a heartbeat | Prove the node runs the code it claims and reports the schema it has | scripts/alignment_report.py + umbra_node_report.py produce the payloads; blocked on the controller issuing a compute:read + compute:heartbeat credential |
30 min once credentialed |
M4-11 |
Alignment report tool (controller §6.6) | Agreement must be mechanical, not prose | done 2026-10-03 — scripts/alignment_report.py --phase all, per-phase evidence shapes, blocked never pass without evidence |
1 h |
9.2 NEXT — needs a decision or another machine
| ID | Task | Blocked by |
|---|---|---|
M4-09 |
Resolve remaining orphan name tokens (10 in Last Dwarf) | manifest.md §15.1 policy; §7.2 Q5 |
M4-10 |
Merge designations that are the same person ("The Big One" / "The Biggest One") | Human review |
M4-11 |
Re-run attribution after the designation merge | M4-10 |
M4-12 |
Make PROJECT_BASE configurable |
Touches config.py, which a parallel session is editing — coordinate first |
M4-13 |
Decide the object-storage host | DECISION-6 (§7.5 Q22) |
M4-14 |
Backfill source_unit_key for existing utterances |
M4-04 + a definition of the key inputs |
9.3 LATER — needs the shared database
| ID | Task | Blocked by |
|---|---|---|
M4-15 |
Implement the read-only mirror (P2) | DECISION-1 ratified; B1/B4 fixed on the controller |
M4-16 |
Prove the import creates no file in the source tree | M4-15 |
M4-17 |
Lease + heartbeat client so a task can be claimed | service_identities + job_leases exist |
M4-18 |
Definition-set pull and version reporting | Controller serves the set |
M4-19 |
Per-project authoritative cutover |
P2/P3 verification green for that project |
M4-20 |
Per-locale voice assets (vmatrix per (voice, locale)) |
manifest.md §15.11 decision |
9.4 Sequencing note
M4-04 and M4-05 are the highest-leverage items on this list. Both are additive, both are
independent of the controller, and both make every later phase cheaper. If the system does
nothing else, do those two.
10. Decision record — utterance_index (controller §5.7)
The controller asked this node to record the decision rather than make it for us.
utterance_indexis KEPT as a display ordinal, and is made non-load-bearing.
- Kept because a reviewer needs "utt 231" to find a line, and because deleting or
renumbering it now breaks nothing that matters:
source_unit_keyis the identity, the column is a label. - Non-load-bearing means: no artifact name, no lineage record, no cross-reference and
no cross-language link may use it. Audio filenames still do (
utt_{index:04d}.wav) as a local filesystem convenience; the marker now also carriessource_unit_key, so the audio record can name its source unit without the filename being load-bearing. - Recorded by: the UmbraAudio node, 2026-10-03. Status: node decision, open to reversal.
Proof that the decision was actually implemented, not just written: renumbering a chapter by
+100 changed 0 of its keys, and 200/200 probe units resolved with used_index: false.
11. Gaps found in the controller's alignment proposal
The controller asked to be told about a fifth gap rather than have it worked around. Six, in the order they matter.
| ID | Severity | Gap | What this node did |
|---|---|---|---|
| G-A | major | "Every blob gets its hash at generation" — but audio is post-processed after generation (apply_effects_to_file, distortion effects, scene-break silence). A hash taken at generation would identify bytes that never shipped, and a changed trait list would silently change the artifact with no recorded cause. |
Verified the existing order is correct (write_marker runs after effects), and closed the remaining hole: the marker now records effects: [...] alongside the hash, so the chain from raw TTS output to delivered bytes is explicit. |
| G-B | major | source_unit_key is anchored to chapters.source_text — a second stored copy of the copyrighted manuscript. Any change to how chapters are stored (unicode normalisation, trimming, re-extraction) silently invalidates every key in that chapter. |
Documented as a deliberate, load-bearing coupling: content identity needs a content anchor. Mitigation is the anchor's own hash, now reported per project. Recommend the controller treat the chapter-text hash as the source revision identity rather than assuming a separate revision id. |
| G-C | major | The controller's key spec includes source_revision_id. This node has no revision concept, so it substitutes a chapter-text hash. Two sides will therefore compute different keys for the same unit. |
Answered rather than worked around: source_unit_key is the audio-side key and the controller must store it verbatim as source_units.source_unit_key rather than recomputing it. Recomputation would be impossible anyway — the controller cannot know our occurrence rule. |
| G-D | minor | The marker did not record which source unit produced a clip, so an audio artifact could not be traced back to a unit revision. | Marker now records source_unit_key. |
| G-E | major | Voice assets are keyed by name-derived slugs (officer_ref.wav). Two projects can hold the same slug for different voices, so a slug is not a portable identity. |
The node report now emits each ref WAV's full sha256 + object_key. voice_assets must key on the bytes and carry the project scope; the slug stays a local label. |
| G-F | minor | source_unit_key is project-agnostic by content: identical chapter text in two projects yields identical keys. That answers the controller's open §8.3 question — and it is a tenancy consideration, not only a design one. |
Deliberately left unsalted and documented. If per-tenant isolation is required, the controller salts at ingest (`sha256(tenant |
12. Alignment status as of 2026-10-03
| Phase | Verdict | Note |
|---|---|---|
| 0 (reachability) | pass | health 200, heartbeat 401, missing-Host 400 — all three exactly as documented |
| 1 (vocabulary) | pass | 19 sets, 126 values, published to definitions/umbraaudio-defs.json with per-value provenance |
| 2 (cardinality) | fail | Evidence present; the three-way store disagreement is real and unresolved — a pass here would be a check that passed by doing nothing |
| 3 (identity) | pass | 200/200 units resolved by content key, 0 used utterance_index |
| 4 (lineage) | blocked | needs the controller's chosen artifact |
| 5 (timeline) | blocked | no time-scoped attribute model exists here yet |
| 6 (voice matrix) | blocked | voices are not captured as structured matrix values |
| 6b (persistence/partial capture) | blocked | persistence is not captured at all; absence is NULL, not typed |
| 6c (editions) | blocked | no edition concept |
| 7 (round trip) | blocked | needs the ingest API, which does not exist |
Regenerate any row with scripts/alignment_report.py --phase all.
13. Change log
| Date | Change |
|---|---|
| 2026-10-01 | Initial draft. Created by the UmbraAudio node. Added infra/umbra-node.json, scripts/umbra_node_report.py. Status: awaiting ratification. |
| 2026-10-02 | Controller (oldbox) alignment proposal received; D1-D16 recorded, 2 questions open. This document is the node's response. |
| 2026-10-03 | Controller alignment proposal actioned. Implemented source_unit_key (§10 decision, §5.1), blob hashes + effect chain (§5.2), published vocabularies (§5.4), fingerprint discipline (§5.5), and the alignment report tool (§6.6). Recorded six gaps (§11) and phase-by-phase status (§12). Stage 0 verified. |
| 2026-10-03 | Controller alignment proposal received and actioned. Implemented source_unit_key (§10 decision, §5.1), blob hashes + effect chain (§5.2), published vocabularies (§5.4), fingerprint discipline (§5.5), and the alignment report tool (§6.6). Recorded six gaps (§11) and the phase-by-phase status (§12). |