fix: seventeen devices from the poka-yoke audit (#705–#721) - #722
Conversation
Closes #706. Closes #710. Closes #714. Closes #716. Refs #720. Five of the sixteen findings in the 2026-09-04 audit, chosen as the ones whose device reaches CONTROL for a bounded cost. The rest are filed with their proposed rung and are not in this change. #706 -- A PASSWORD CHANGE LEFT ITS TOKENS' SOCKETS STREAMING. Revocation had two halves: delete the row, and mark the id in an in-process set the /ws tick reads. handleRevokeAPIToken did both; handleChangePassword, which calls DeleteAllAPITokens, marked nothing. A socket opened with an admin token kept receiving admin-shaped events until it or the process died, while the response correctly reported zero surviving tokens. The revoke handler had predicted it exactly: "a second deletion path that did not do this would be a silent hole." handleChangePassword was that path. The set is now GONE. The tick asks the store (db.APITokenExists), the way the session half beside it already asks for users.token_epoch, and fails closed on a read error for the reason stated there. That is Control rather than Warning: there is no second half left to forget, so resetadmin (#718) and whatever is added next are honoured without being told to be. Cost is one indexed read per socket per ping period, which is what the epoch check already costs. Its test asserted the old premise -- that a deletion bypassing the handler is invisible -- and closed with "if something else now writes to it, this test's premise is stale". Something did. It now pins the stronger property, that a deletion made anywhere ends the socket, and keeps the half that still matters: this check closes sockets early and is not an authorisation source. #710 -- requireScope passed a request with NO PRINCIPAL. Its siblings requireSession and requireCSRF both fail closed on the same condition. Every group carrying it today also carries requireAuth, so this changes no live behaviour; it removes the affordance of mounting them apart or in the wrong order. The p.token == nil arm is a different case and stays: a session principal is not a scoped token. #714 -- stopAux took (slot, name) and recovered the rest from a switch. Two silent mistakes were available: e.stopAux(&e.preview, "recorder") compiles and reads correctly while releasing the wrong port and leaking the preview's, and the switch had no default, so a fourth consumer would skip its release and leave reconcile believing the child was up. One auxSlot value per consumer means there is no second argument to mismatch and no switch to fall through. It also brings these three into line with every other subscriber in the package, which stores its name at subscribe time rather than recomputing a literal at teardown. The test table now names the PRODUCTION slots instead of carrying its own copy of the same correspondence. #720 -- kill() gained the reaped-check its two siblings carry. killGroup issues a raw syscall.Kill(-pid, SIGKILL), which names a process group by number and bypasses Go's ErrProcessDone; on a reaped pid that can signal a group this supervisor never started. The safety argument was correct and was a comment spanning three functions. Now it is a guard. #716 -- AND THE REGRESSION IT CAUSED, recorded rather than quietly fixed. Making the policy switch fail closed dropped every passthrough event -- Log, Status, Levels, Stats, Loudness -- because wsPassthrough is `iota`, the zero value, and had no case of its own: it had always reached the sender through the default arm being replaced. The opening burst on a fresh install delivered nothing, and TestTheWebSocketOpeningBurstSurvivesZeroSources caught it. That is the general hazard of tightening a default, and it is now a comment at the case: the arm being replaced has to be enumerated first, and a zero-valued constant is the member most likely to be sitting in it unnoticed. wsPassthrough is explicit now, and default still fails closed for a constant added later -- which was the point, since passing an unknown policy through sends a read-scoped socket the admin shape. Two mutations, each red: the socket no longer asking the store, and passthrough falling back to the closed default. internal/api, internal/engine, internal/supervisor and internal/db all green. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
There was a problem hiding this comment.
🟡 Changes recommended
A couple of newly introduced/modified documentation comments are now misleading (e.g., token existence and WS revocation tick behavior) and should be corrected to avoid future maintenance mistakes.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR implements four targeted fixes from the poka‑yoke audit (plus the noted #716 regression follow-up), tightening correctness in WebSocket revocation/policy handling, auth middleware ordering safety, engine aux teardown safety, and supervisor signalling safety.
Changes:
- WebSocket hardening: replace the process-local revoked set with a store-backed
APITokenExistscheck, and make unknown WS policy values fail closed (with an explicitwsPassthroughcase). - Auth & supervisor safety: make
requireScopefail closed when there is no principal, and add a reaped-check guard toProcess.kill()before issuingsyscall.Kill(-pid, SIGKILL). - Engine aux teardown safety: replace
(slot, name)teardown with a singleauxSlotdescriptor to prevent mismatched teardown/unsubscribe/release, and update lifecycle tests accordingly.
File summaries
| File | Description |
|---|---|
| internal/supervisor/supervisor.go | Adds a reaped-check guard to kill() to avoid signalling unrelated process groups. |
| internal/engine/engine.go | Refactors aux teardown to use auxSlot and eliminates name/slot mismatch hazards. |
| internal/engine/lifecycle_test.go | Updates aux teardown tests to use production auxSlot values and validate isolation. |
| internal/db/tokens.go | Adds APITokenExists store query to support WS revocation via the DB. |
| internal/api/ws.go | Switches WS revocation check to tokenRevoked (store-backed). |
| internal/api/ws_revoke_test.go | Updates revocation tests to assert “deletion anywhere ends the socket” property. |
| internal/api/ws_policy.go | Makes passthrough explicit and defaults to fail-closed for unknown policies. |
| internal/api/token_handlers.go | Removes the revoked-set write (set no longer exists). |
| internal/api/api.go | Removes revoked set, adds tokenRevoked store-backed logic, and makes requireScope fail closed on missing principal. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // as a reason not to return would keep the socket that this branch | ||
| // exists to end. | ||
| if s.isRevoked(tokenID) { | ||
| if s.tokenRevoked(tokenID) { |
| // DeleteAPIToken revokes a token. | ||
| // APITokenExists reports whether a token row is still there. |
Closes #705. Closes #712. Closes #713. Closes #718. Closes #721. #705 -- fsperm's ACL builder stored SIDs as unpinned integers. TrusteeValueFromSID converts a *windows.SID to a uintptr and x/sys says in as many words: "The caller must pin sid using a runtime.Pinner for the lifetime of the TrusteeValue." Both SIDs are Go-allocated and collectable, and the EXPLICIT_ACCESS outlives the call. The pinner is taken in restrict rather than in grantFull -- the version that looks correct and is not, because a pinner there unpins on return, before ACLFromEntries ever reads the trustee. Consequence was a wrong DACL, not heap corruption, on the files holding every stream key. #712 -- destWritesAFile now fails CLOSED on a kind this build does not know. db.DestKind and ffmpeg.DestKind are declared independently and joined by a cast that compiles for any string, so a fifth kind reaches the default. Returning false there was wrong for the reason the function's own comment gives: without the confinement an audio file target is written relative to the process working directory. An unknown kind is now treated as a file writer and confined, and RTMP/SRT are named rather than left to the default so the exhaustiveness is visible. #718 -- resetadmin printed "every existing session has been signed out" and stopped. True and incomplete: the epoch ends SESSIONS, while API tokens are resolved by hash alone and survive. This is the command an operator reaches for when they cannot sign in, which is the compromise case, and it was the one path that could not say what still had access. It now always lists the surviving tokens and how to end them, mirroring the disclosure the HTTP handler already performs, and --revoke-api-tokens does the ending. Opt-in rather than implied, for the reason that handler argues: routine rotation is the common case and destroying every integration's credential is the wrong default for it. #721 -- isSetup and setupSlot are two switches over one closed type set, folded into a single condition in cacheSetup that made "ordinary media" and "setup with no slot" indistinguishable. A test now asserts they agree on every handled type. The runtime log the issue also proposed was NOT taken: `stream` has no logger, threading one in for this is not worth it, and a build-time comparison catches the disagreement when it is written rather than when it hangs somebody's ffprobe. Two mutations, each red -- dropping a type from either switch. #713 -- AND THE CODE DISPROVED THE FINDING AS FILED, which is recorded rather than quietly fixed. The issue says Rumble, Trovo and Vimeo are "silently exempt from loudness checking" and should get policy rows. Reading internal/db's constants says otherwise: Rumble "exists for CHAT and for nothing else", Vimeo "for SIGN-IN", and Trovo can fetch a stream key but not the ingest URL beside it, so all three are pasted by hand and behave as custom destinations do. They take the custom row deliberately. The hazard the issue identified is still real, and it is the other half of its own sentence: nothing distinguishes "custom by choice" from "custom because the table forgot you". So the device is a test that forces the decision to be STATED -- an opt-out list carrying the reason per platform, with a second test refusing a placeholder reason and refusing to let the list grow past four. Three silent omissions become three stated decisions, and a fourth fails. Two mutations red. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…le release #707 and #708, plus coverage for the devices the previous commit installed. THE PORT LEDGER. The relay pool is 500 ports shared across every engine, and Manager.Sync stops an engine on every source delete while the daemon keeps running -- so a port an engine fails to return is gone for the life of the process. Nothing reports it until Allocate starts failing, and then it fails everywhere at once and reads as an unrelated fault. Four kinds were not returned. Destinations, loudness, clips, captions, feeds, silence, backup and playlist went through a teardown that released; the recorder, preview, meters and renditions went through a bare `stop` helper that only called Stop. Measured before the fix: three ports plus one per rendition leaked per shutdown, and the hub kept their subscriptions. Engine.allocPort/releasePort now keep the ledger, so a release names a port THIS engine holds or is refused and logged (#708). Release took a bare int and did delete(a.held, p), so releasing twice silently un-held a port the pool may have since handed to a different engine -- two engines on one UDP port, which is what the allocator's bind probe exists to prevent. The probe only masks it while the first owner's child is bound; during restart backoff it is open. StopWithin carries a post-condition on heldPortCount. Each of the three leaks was mutation-tested red before the fix went in. COVERAGE FOR THE DEVICES. Eight guards from the previous commit had no test that had watched them fail, which is the one thing this audit says not to ship: - APITokenExists on a live, deleted, zero and absent id (#706) - tokenRevoked's fail-closed arm, reached by closing the store underneath it - requireScope with no principal in the context (#710) - eventView's explicit wsPassthrough case, and BOTH fail-closed arms -- the unclassified type, and a policy constant with no case, reached by planting one in the table (#716) - destWritesAFile on a kind this build does not know (#712) - kill() on a reaped pid, against the guard directly: the supervisor clears p.exited during teardown, so waiting for a real reap races the field the guard reads (#720) - resetadmin's disclosure when it cannot revoke and cannot read the token list, reached by breaking the table in two ways that survive db.Open (#718) Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
… to an unsigned POST Closes #715. Restore a backup taken on another machine, or rotate secret.key, and every webhook silently went out UNSIGNED. db.scanHook loaded the row with an empty Secret when box.Open failed and said nothing; dispatch.attempt skips the signature header on an empty secret, so the delivery went anyway. At the far end that is indistinguishable from a forgery. The signing secret was the only thing that made the delivery trustworthy, and its absence is invisible to the receiver: an endpoint that verifies rejects everything, and one that does not now accepts anything anybody sends it. internal/db/platforms.go handled the identical situation the other way and failed loud, so the two halves of the same decision disagreed. Hook.SecretUnreadable is the same device Destination.KeyUnreadable already is, for the same reason and with the same shape: set by the scanner and never by a column, because it is a fact about this process's key file rather than about the row. Restore the key file and it goes away by itself, with no repair step and nothing to un-set. The row still loads intact -- failing the read would take every other hook down with it and turn a lost key file into a hooks page that will not load. Three places refuse it, and the order matters: - Validate, so reload never starts a worker and there is no queue for a delivery to sit in; - attempt, the one place the signature is decided and the only path BOTH delivery and the operator's Test button reach -- this is what makes an unsigned delivery unreachable rather than merely filtered; - reload logs at ERROR for this refusal and stays quiet for the others, since every other one is something the operator typed and was already told about at save time, while this one is a machine fact they can fix and would otherwise learn from a hook that stopped firing. Three mutations, each red: scanHook not marking the row, Validate accepting it, and attempt posting anyway. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…lently replacing the consumer Closes #711. Hub.SubscribeAddr was a bare map assignment: h.subs[name] = &subscriber{...} Registering two consumers under one name REPLACED the first. It kept running, kept a correct command line, kept a green card on the monitoring page -- and received nothing. Nothing about the process, its target URL or its status revealed it, and the hub logged "relay subscriber added" either way, so the log positively confirmed the wrong thing. Three devices already existed to avoid the collision and all three were rung zero: a naming convention (destinations.go's role suffix), a lock (engine.go's reconcileMu) and a comment (setup.go's note about the RTMP key variant). It had bitten twice. The sink itself accepted the collision without a word. Subscribe and SubscribeAddr now return (string, error) and refuse an occupied name with ErrSubscriberExists, logging at ERROR with BOTH addresses -- the whole difficulty of the old failure was that everything downstream looked healthy, so the one place it can be seen is the refusal itself. Thirteen production call sites change signature. Each one already had a release-and-bail path for the allocator refusing a port, and the subscription refusal takes the same path: release the port, and in the two places that record a subscription name before starting the child (the selector feed and the preview), clear it BEFORE the teardown can unsubscribe a name this consumer never took -- which would cut off whoever actually holds it. Unsubscribe is the mirror and was silent in the same way: delete() on an absent key is a no-op and the line said "removed" regardless, so a teardown naming the wrong subscriber read as successful cleanup. Not an error return -- every caller is a teardown path that would either ignore it or abandon the rest of its cleanup -- but it now says so at ERROR and removes nothing quietly. A test case asserted the hazard: "re-subscribing the same name replaces rather than duplicates". Replacing IS the failure. It now pins the refusal, that the FIRST consumer keeps the name, and that the name is free again after Unsubscribe. The new test asserts against the socket rather than the bookkeeping, because the failure was a consumer going quiet while looking healthy. Test fixtures across relay, engine and rtmpserver take a mustSubscribe helper rather than dropping the error: a fixture that silently fails to register would be asserting fan-out against a hub with no consumer on it. playout's fakeHub refuses collisions too, since a fake that accepts what production refuses hides the bug. Two mutations, each red: the bare map assignment restored, and Unsubscribe reporting a removal it did not make. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…port says what it covered Closes #717. The census was added inside internal/supervisor with unexported enrol/discharge, so exactly ONE of the roughly twenty spawn sites in this repository could use it -- and internal/ffmpeg could not have used it even if it wanted to, because supervisor imports ffmpeg and the import would have been a cycle. Its own comment framed it as "WHAT HAVE WE ACTUALLY SPAWNED?" and the shutdown report claimed it "would have said it on the first occurrence" of #631. Both were true only for supervisor children. A transcode or a whisper child surviving shutdown produced exactly the silence #631 produced -- while the shutdown log actively reported that nothing was wrong. A detection device that under-reports is worse than none, because its green is read as an all-clear. Three parts: - internal/childcensus, a leaf package importing nothing from this module, so every spawner can reach it. Enrol and Discharge are exported. - The spawners that can outlive a call now enrol: media.Exec (every transcode and derivative encode), the live-caption whisper and audio tap, and the transcription extract and whisper workers. Discharge is paired at the Wait, which is the only moment the pid is genuinely gone. - TestEverySpawnSiteIsAccountedFor walks every non-test .go file in the tree and requires each package calling exec.Command to be either a package that enrols, or one listed WITH A REASON saying what bounds its child's life. Seven silent omissions become seven stated decisions and the twenty-first spawner fails the build. The excuses are themselves checked: a reason under 40 characters, or one containing TODO, fails. An entry for a package that no longer spawns anything fails too, so the list cannot rot into a standing excuse nobody re-earns. The walker skips every dot-directory, not just .git: .claude/worktrees holds checkouts of this same repository, and walking them reported every spawn site three times under a path nobody can act on. reportSurvivingChildren now says so on a clean shutdown, and names its scope. Its test asserted the opposite -- "a line here on every clean stop would teach operators to skim past the one that matters" -- and that reasoning was sound when the scope was narrow enough to make silence ambiguous. It now pins the new property and records why it changed. Control is not available: nothing in Go stops a package calling exec.Command. This is Warning, raised when the code is written rather than when a child is found on a host. Also: coverage for the #711 refusal paths, which are the ones that had none. Every aux consumer, the destination and its backup, the silence tier and a playout variant, each driven into a taken name and each asserted on both halves -- nothing started, and the port came back. Closing the collision hazard must not open the leak hazard #707 was about. Three mutations, each red: a spawner that stops enrolling, an excuse reduced to a placeholder, and the clean shutdown going back to saying nothing. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…plain success Closes #709. Sixteen handlers called s.reconcile() and TWO SPELLINGS lived side by side in one package: three turned the error into a 500, twelve logged it at Warn and returned success. Nothing in the signature said which was right, so a handler written by copying its nearest neighbour got whichever that neighbour was. The silent spelling is worse than it looks. Engine.Reconcile returns early on a reconcileOutputs error, so preview, clips, captions and loudness are skipped with it; Manager.Reconcile returns firstErr. And reconcile is EVENT-DRIVEN WITH NO TICKER -- engine.go says so in as many words -- so the failure is never retried. Stored state and the running FFmpeg diverge until the next successful mutation or a restart, while the response said 200 and the UI raised a green toast. The worst case was invisible: handleDeleteDestination returned {"status": "deleted"}, the row left the list, and the FFmpeg child kept publishing to a destination the console no longer draws. ONE SPELLING. Server.reconcileNow is the only caller of s.reconcile() and returns the sentence to hand the operator, empty on success. The three loud sites turn a non-empty result into their 500; the rest attach it. What is gone is the per-site choice about whether the failure is reported at all. writeMutation folds the sentence into the response THROUGH THE MARSHALLED FORM rather than a field on each of a dozen response types -- adding a field per type is exactly the per-site decision being removed. It appends to `warnings`, which two handlers already build and the SPA already renders, rather than inventing a second array meaning the same thing, and sets reconcileFailed so the UI branches on a flag rather than on an English sentence. writeMutationNoContent handles the 204 routes: a 204 has no body, so a failed reconcile becomes a 200 that can say something rather than an empty success. expert.go was the sharpest instance and is fixed as such. Applied shipped as a hardcoded `true`, and a comment claimed "the command shown back is the one the reconcile above just started" -- false on exactly the branch that swallowed the error. Applied is now `rw == ""` and Warning carries the reason, which MonitoringPage already renders. scheduler.Result gains ReconcileErr, set on every schedule that FIRED in a sweep whose reconcile failed. On every one, because the reconcile is a single call for the batch: which of them took effect is not knowable, and saying so on each is honest where attributing it to one would not be. THE UI HALF IS IN THE TRANSPORT, not at the call sites. api.ts's request() raises the amber toast when it sees reconcileFailed, so all fifteen routes and every route added later are covered without being told. It reads the FLAG rather than the array, so a warning the server attached for another reason -- a DestinationDialog platform note -- is not doubled. TestReconcileHasOneSpellingAndItsResultCannotBeDropped enforces both rules at build time: s.reconcile() must have exactly one caller, and a bare `s.reconcileNow(...)` statement fails, because Go will happily let you discard the returned string and discarding it is precisely the mistake. Warning rather than Control, deliberately: refusing the write would be wrong. The row genuinely was saved, and a 500 invites a retry that re-POSTs a destination. The honest ceiling is that the response states what did not happen. Six mutations, each red: a handler dropping the warning, a second s.reconcile() spelling reappearing, writeMutation dropping it, an existing warnings array being overwritten, the transport not reading the flag, and the transport reading the array instead of the flag. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…s the operator to act Closes #719. lib/readState.ts exists to make `.catch(() => setX([]))` unwriteable, and its header calls itself "a CONTROL rather than a warning" because mayClaim is a type guard. DestinationDialog.tsx never imported it and had FIVE of them. Two of the five drove the operator to act on a claim the server never made: - a failed listRenditions() rendered "No shared encodes yet. Create one on the Renditions page first." and disabled the shared-encode radio, sending them to build a second real encode they already have; - a failed listAccounts() replaced the account picker with "Connect {platform} account" -- a re-authorisation for an account that is very likely already linked, and following it navigates away and discards the half-filled dialog. All five reads now carry a ReadState. The arrays are derived with rowsOf, so every consumer that only wants rows is untouched; mayClaim guards the two places that say something, and readFailed gives each a sentence that distinguishes "we could not ask" from "there are none". WHAT ENFORCED THE RULE WAS A LIST OF FILENAMES. readState.test.ts asserted that specific source strings were absent from SettingsPage.tsx and AutomationPage.tsx -- three files, hand-maintained. That is training, not a device, and it did not know DestinationDialog.tsx existed. readState.shape.test.ts walks every .ts/.tsx under ui/src and matches on the SHAPE instead, so the file added tomorrow is covered without being listed. It found nine more instances the audit never named. Each is now a STATED decision in an excuse map keyed by file AND setter -- a file-level key would have covered DestinationDialog's other three by accident, which is exactly how the previous enforcement missed it. Most turn out to be `null` used as a genuine third state with the consumer already branching on it; those carry the reason, and the reasons are themselves checked for length and placeholders, and for still matching a read that exists. One was a real bug. Dashboard's metadata composer holds `MetaTarget[] | null` and `if (targets === null) return null` hides it entirely -- but the catch stored [], so a failed read rendered the composer with ZERO platforms. That is a positive claim that this broadcast has nowhere to push metadata. It is null now. The guard bans okRead([]) inside a catch as well as a bare [], because adopting ReadState and then storing a successful-looking empty read is the same mistake wearing the fix, and the type no longer objects to it. Found by mutation: the first version of the guard passed that spelling. Comments are blanked before matching, with newlines preserved so reported line numbers still point at the file: a guard that fires on prose describing the hazard is a guard people delete rather than obey. It also carries positive and negative controls over the pattern itself, because the regex IS the device and one that quietly stopped matching would leave every assertion passing over nothing. Warning rather than Control: nothing in TypeScript stops a component holding a bare array and assigning [] in a catch. The earliest a device can speak is when the code is written. Four mutations, each red: a dialog read storing [] again, the same read storing okRead([]), an excuse reduced to a placeholder, and an excuse outliving the read it excused. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
|



All seventeen findings from the poka-yoke audit, each with the device its
rung called for.
Closes #705, #706, #707, #708, #709, #710, #711, #712, #713, #714, #715, #716,
#717, #718, #719, #720, #721.
Every guard here has a test that has watched it fail — a planted mutation,
or a fixture that reaches the refusal arm. That is the audit's own standard: a
guard nobody has watched fail is a guard nobody should trust.
#706 — a password change left its tokens' sockets streaming
Revocation had two halves: delete the row, and mark the id in an in-process
set the
/wstick reads.handleRevokeAPITokendid both.handleChangePassword, which callsDeleteAllAPITokens, marked nothing.A socket opened with an admin token kept receiving admin-shaped events until it
or the process died — while the response correctly reported zero surviving
tokens. The revoke handler had predicted it word for word:
The set is gone. The tick asks the store (
db.APITokenExists) the way thesession half beside it already asks for
users.token_epoch, failing closed on aread error for the reason stated there. That is Control rather than Warning:
there is no second half left to forget, so
resetadmin(#718) and whatever isadded next are honoured without being told to be. One indexed read per socket
per ping period — what the epoch check already costs.
Its test asserted the old premise and closed with "if something else now
writes to it, this test's premise is stale". Something did. It now pins the
stronger property — a deletion made anywhere ends the socket — and keeps the
half that still matters: this check closes sockets early and is not an
authorisation source.
#710 —
requireScopepassed a request with no principalIts siblings
requireSessionandrequireCSRFboth fail closed on the samecondition. Every group carrying it today also carries
requireAuth, so thischanges no live behaviour — it removes the affordance of mounting them apart or
in the wrong order. The
p.token == nilarm is a different, deliberate case andstays: a session principal is not a scoped token.
#714 —
stopAuxtook a slot and a name that had to correspondTwo silent mistakes were available:
e.stopAux(&e.preview, "recorder")compilesand reads correctly while releasing the wrong port and leaking the preview's;
and the
switchhad nodefault, so a fourth consumer would skip its releaseand leave reconcile believing the child was up.
One
auxSlotvalue per consumer — no second argument to mismatch, no switch tofall through. It also brings these three into line with every other subscriber
in the package, which stores its name at subscribe time rather than recomputing
a literal at teardown. The test table now names the production slots instead
of carrying its own copy of the same correspondence.
#720 —
kill()gained the reaped-check its two siblings carrykillGroupissues a rawsyscall.Kill(-pid, SIGKILL), naming a process group bynumber and bypassing Go's
ErrProcessDone. On a reaped pid that can signal agroup this supervisor never started. The safety argument was correct — and was a
comment spanning three functions. Now it is a guard.
#716 — and the regression it caused
Recorded rather than quietly fixed, because it is the general lesson.
Making the policy switch fail closed dropped every passthrough event — Log,
Status, Levels, Stats, Loudness — because
wsPassthroughisiota, the zerovalue, and had no case of its own: it had always reached the sender through the
defaultarm I was replacing. The opening burst on a fresh install deliverednothing, and
TestTheWebSocketOpeningBurstSurvivesZeroSourcescaught it.That hazard is now a comment at the case: the arm being replaced has to be
enumerated first, and a zero-valued constant is the member most likely to be
sitting in it unnoticed.
wsPassthroughis explicit, anddefaultstill failsclosed for a constant added later — which was the point, since passing an
unknown policy through sends a read-scoped socket the admin shape.
#707 and #708 — the engine leaked a relay port per aux child, and
Releasehad no ownerThe pool is 500 ports shared across every engine, and
Manager.Syncstopsan engine on every source delete while the daemon keeps running — so a port an
engine fails to return is gone for the life of the process. Nothing reports it
until
Allocatestarts failing, and then it fails everywhere at once andreads as an unrelated fault.
Four kinds were not returned. Destinations, loudness, clips, captions, feeds,
silence, backup and playlist went through a teardown that released; the
recorder, preview, meters and renditions went through a bare
stophelper thatonly called
Stop. Measured before the fix: three ports plus one perrendition leaked per shutdown, and the hub kept their subscriptions.
Engine.allocPort/releasePortnow keep a per-engine ledger, so a releasenames a port this engine holds or is refused and logged (#708).
Releasetook a bare
intand diddelete(a.held, p), so releasing twice silentlyun-held a port the pool may have since handed to a different engine — two
engines on one UDP port, which is what the allocator's bind probe exists to
prevent. The probe only masks it while the first owner's child is bound; during
restart backoff, or between
AllocateandStart, it is open.StopWithincarries a post-condition on
heldPortCount.The reachable double-release path:
stopBackupdeliberately does not cleard.backupPort, so the struct still names a released port and a later teardownreleases it again.
#709 — a mutation whose reconcile failed answered with a plain success
Sixteen handlers called
s.reconcile()and two spellings lived side by sidein one package: three turned the error into a 500, twelve logged it at
Warnand returned success. Nothing in the signature said which was right, so a
handler written by copying its nearest neighbour got whichever that neighbour
was.
The silent spelling is worse than it looks.
Engine.Reconcilereturns early ona
reconcileOutputserror, so preview, clips, captions and loudness are skippedwith it — and reconcile is event-driven with no ticker, so the failure is
never retried. Stored state and the running FFmpeg diverge until the next
successful mutation or a restart, while the response said 200 and the UI raised
a green toast.
The worst case was invisible:
handleDeleteDestinationreturned{"status":"deleted"}, the row left the list, and the FFmpeg child keptpublishing to a destination the console no longer draws.
Server.reconcileNowis now the only caller ofs.reconcile().writeMutationfolds its sentence into the response through the marshalled form rather than
a field on each of a dozen response types — adding a field per type is the same
per-site decision being removed.
expert.goshippedApplied: trueas aliteral beside a comment claiming "the command shown back is the one the
reconcile above just started", false on exactly the swallowing branch.
The UI half is in the transport, not at the call sites:
api.ts'srequest()raises the amber toast onreconcileFailed, so all fifteen routesand every route added later are covered without being told.
#711 — a relay subscriber name collision silently replaced one consumer
h.subs[name] = &subscriber{...}was a bare map assignment. The replacedconsumer keeps running, keeps a correct command line, keeps a green card — and
receives nothing. The hub logged "relay subscriber added" either way, so the
log positively confirmed the wrong thing.
Three devices already existed to avoid the collision and all three were rung
zero: a naming convention, a lock, and a comment. It had bitten twice.
Subscriberefuses an occupied name. Thirteen production call sites changesignature; each already had a release-and-bail path for the allocator refusing a
port.
Unsubscribewas the mirror —deleteon an absent key is a no-op andthe line said "removed" regardless.
A test case asserted the hazard: "re-subscribing the same name replaces rather
than duplicates." Replacing is the failure.
#715 — an unreadable hook secret degraded to an unsigned POST
Restore a backup taken on another machine, or rotate
secret.key, and everywebhook went out unsigned.
scanHookloaded the row with an emptySecretand said nothing;
attemptskips the signature header on an empty secret.At the far end that is indistinguishable from a forgery.
Hook.SecretUnreadableis the device
Destination.KeyUnreadablealready is. Three places refuse it,and
attempt— the one place the signature is decided, reached by both deliveryand the operator's Test button — is what makes an unsigned delivery unreachable
rather than merely filtered.
#717 — the census covered one of about twenty-five spawn sites
enrol/dischargewere unexported insideinternal/supervisor, so one spawnercould use it — and
internal/ffmpegcould not have, because supervisor importsffmpeg. Its own comment framed it as "WHAT HAVE WE ACTUALLY SPAWNED?", true
only for supervisor children, while the shutdown report said nothing at all on a
clean stop.
internal/childcensusis a leaf package every spawner can reach. Mediatranscodes and the transcription and live-caption workers enrol.
TestEverySpawnSiteIsAccountedForwalks the tree and fails the build for apackage that spawns without enrolling and without a stated reason: seven silent
omissions became seven stated decisions.
#719 — the UI reported a failed read as an empty result
lib/readState.tsexists to make.catch(() => setX([]))unwriteable.DestinationDialog.tsxnever imported it and had five. Two drove theoperator to act on a claim the server never made: "No shared encodes yet.
Create one on the Renditions page first." and a Connect account button for
an account already linked, which discards the half-filled dialog.
What enforced the rule was a list of three filenames — training, not a device,
and it did not know this file existed.
readState.shape.test.tswalks everyfile and matches on the shape, and found nine more the audit never named.
One was a real bug: the Dashboard's metadata composer hides itself on
nullbutthe catch stored
[], so a failed read rendered the composer with zeroplatforms.
#705 —
fsperm's ACL builder stored SIDs as unpinned integersTrusteeValueFromSIDconverts a*windows.SIDto auintptr, andx/syssaysit in as many words: "The caller must pin sid using a runtime.Pinner for the
lifetime of the TrusteeValue." Both SIDs are Go-allocated and collectable, and
the
EXPLICIT_ACCESSoutlives the call.The pinner is taken in
restrictrather than ingrantFull— the version thatlooks correct and is not, because a pinner there unpins on return, before
ACLFromEntriesever reads the trustee. Consequence was a wrong DACL, not heapcorruption, on the files holding every stream key.
#712 —
destWritesAFilefailed open on a kind this build does not knowdb.DestKindandffmpeg.DestKindare declared independently and joined by aconversion that compiles for any string, so a fifth kind reaches the
default.Returning
falsethere was wrong for the reason the function's own commentgives: without the confinement an audio file target is written relative to the
process working directory, outside the directory every other file destination is
held to. An unknown kind is now treated as a file writer and confined, and
RTMP/SRT are named rather than left to the default so the exhaustiveness is
visible to a linter.
#718 —
resetadmincould not say what still had accessIt printed "every existing session has been signed out" and stopped. True and
incomplete: the epoch ends sessions, while API tokens are resolved by hash
alone and survive. This is the command an operator reaches for when they cannot
sign in — the compromise case — and it was the one path that could not say what
still reached the install.
It now always lists the surviving tokens and how to end them, mirroring the
disclosure the HTTP handler already performs, and
--revoke-api-tokensdoes theending. Opt-in rather than implied, for the reason that handler argues: routine
rotation is the common case and destroying every integration's credential is the
wrong default for it.
Its two failure arms matter most, because they fire exactly when the command
cannot answer the question — and an operator not told that silence means
unknown reads it as nothing. Both are tested, reached by breaking the token
table in two ways that survive
db.Open.#721 —
isSetupandsetupSlotare two switches over one closed type setFolded into a single condition in
cacheSetup, which made "ordinary media" and"setup with no slot" indistinguishable. A test now asserts they agree on every
handled type. The runtime log the issue also proposed was not taken:
streamhas no logger, threading one in for this is not worth it, and abuild-time comparison catches the disagreement when it is written rather than
when it hangs somebody's ffprobe.
#713 — the code disproved the finding as filed
Recorded rather than quietly fixed. The issue says Rumble, Trovo and Vimeo are
"silently exempt from loudness checking" and should get policy rows. Reading
internal/db's own constants says otherwise: Rumble "exists for CHAT and fornothing else", Vimeo "for SIGN-IN", and Trovo can fetch a stream key but not
the ingest URL beside it — so all three are pasted by hand and behave as custom
destinations do. They take the custom row deliberately.
The hazard it identified is still real, and it is the other half of its own
sentence: nothing distinguished "custom by choice" from "custom because the
table forgot you". So the device is a test that forces the decision to be
stated — an opt-out list carrying the reason per platform, with a second
test refusing a placeholder reason and refusing to let the list grow past four.
Three silent omissions become three stated decisions, and a fourth fails.
Verification
Mutation-tested, each red before the fix: the socket no longer asking the
store; passthrough falling back to the closed default; the aux ports not being
released; the rendition port not being released; a port not held being released
anyway; a type dropped from either side of the setup switch pair; a platform
removed from the routing decision list.
Coverage for the guards themselves, since a guard nobody has watched fail is
a guard nobody should trust:
APITokenExistson a live, deleted, zero andabsent id;
tokenRevoked's fail-closed arm, reached by closing the storeunderneath it;
requireScopewith no principal in the context;eventView'sexplicit passthrough case and both fail-closed arms — the unclassified type,
and a policy constant with no case, reached by planting one in the table;
destWritesAFileon a kind this build does not know;kill()on a reaped pid,tested against the guard directly because the supervisor clears
p.exitedduring teardown and waiting for a real reap races the field the guard reads.
internal/api,internal/engine,internal/supervisor,internal/dbandcmd/polyemesisall green.https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL